From 6efa2b3bce403635269c446b0e376c1124238efb Mon Sep 17 00:00:00 2001 From: Sahilgill24 Date: Tue, 21 Jul 2026 07:09:09 +0530 Subject: [PATCH 01/13] feat: adding state transition function input type --- crates/common/types/src/lib.rs | 1 + crates/common/types/src/stf.rs | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 crates/common/types/src/stf.rs 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..a1c8e053 --- /dev/null +++ b/crates/common/types/src/stf.rs @@ -0,0 +1,28 @@ +use libssz::{SszDecode, SszEncode}; +use serde::{Deserialize, Serialize}; + +use crate::{block::Block, state::State}; + +// we will pass as bytes, as we want this to be definitely serializable and both implement Sszencode and decode +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StfInput { + state: Vec, + block: Vec, +} + +impl StfInput { + pub fn new(state: State, block: Block) -> Self { + StfInput { + state: state.to_ssz(), + block: block.to_ssz(), + } + } + + pub fn return_state(input: Self) -> State { + State::from_ssz_bytes(&input.state).expect("error decoding State") + } + + pub fn return_block(input: Self) -> Block { + Block::from_ssz_bytes(&input.block).expect("error decoding block") + } +} From a1754eda40477a0ba66cc29cf9a8a566110e3040 Mon Sep 17 00:00:00 2001 From: Sahilgill24 Date: Tue, 21 Jul 2026 07:11:05 +0530 Subject: [PATCH 02/13] feat: scaffold sp1 guest program --- Cargo.lock | 260 ++++++++++++++++++--------- Cargo.toml | 2 +- crates/guest-program/sp1/Cargo.toml | 15 ++ crates/guest-program/sp1/src/main.rs | 13 ++ 4 files changed, 204 insertions(+), 86 deletions(-) create mode 100644 crates/guest-program/sp1/Cargo.toml create mode 100644 crates/guest-program/sp1/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 7b514a8c..b5c2a070 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -574,7 +574,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" dependencies = [ - "base64", + "base64 0.22.1", "http", "log", "url", @@ -705,6 +705,12 @@ dependencies = [ "match-lookup", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.22.1" @@ -741,7 +747,7 @@ dependencies = [ "bitflags", "cexpr", "clang-sys", - "itertools 0.12.1", + "itertools 0.13.0", "proc-macro2", "quote", "regex", @@ -1148,6 +1154,12 @@ dependencies = [ "crossbeam-utils 0.8.21", ] +[[package]] +name = "const-default" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b396d1f76d455557e1218ec8066ae14bba60b4b36ecd55577ba979f5db7ecaa" + [[package]] name = "const-hex" version = "1.19.0" @@ -1568,7 +1580,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]] @@ -1865,6 +1877,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "embedded-alloc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f2de9133f68db0d4627ad69db767726c99ff8585272716708227008d3f1bddd" +dependencies = [ + "const-default", + "critical-section", + "linked_list_allocator", + "rlsf", +] + [[package]] name = "embedded-io" version = "0.4.0" @@ -3168,7 +3192,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-util", @@ -4008,7 +4032,7 @@ source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01 dependencies = [ "async-channel", "asynchronous-codec", - "base64", + "base64 0.22.1", "byteorder", "bytes", "either", @@ -4585,6 +4609,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linked_list_allocator" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b23ac50abb8261cb38c6e2a7192d3302e0836dac1628f6a93b82b4fad185897" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -4842,7 +4872,7 @@ source = "git+https://github.com/leanEthereum/leanVM.git?rev=e2592df#e2592df4e30 dependencies = [ "itertools 0.14.0", "mt-utils", - "num-bigint 0.3.3", + "num-bigint 0.4.6", "parallel", "paste", "rand 0.10.1", @@ -4858,7 +4888,7 @@ dependencies = [ "itertools 0.14.0", "mt-field", "mt-utils", - "num-bigint 0.3.3", + "num-bigint 0.4.6", "paste", "rand 0.10.1", "serde", @@ -5327,29 +5357,29 @@ dependencies = [ [[package]] name = "p3-bn254-fr" -version = "0.3.3-succinct" +version = "0.4.3-succinct" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "577200e3fa7e49e2b21e940a6dc7399dc63acb8581da088558cdf7c455adafc0" +checksum = "2077757c7cb514202ccb5368f521f23f5709c720599e6545c683c66e0a52d2d8" dependencies = [ "ff", "num-bigint 0.4.6", - "p3-field 0.3.3-succinct", - "p3-poseidon2 0.3.3-succinct", - "p3-symmetric 0.3.3-succinct", + "p3-field 0.4.3-succinct", + "p3-poseidon2 0.4.3-succinct", + "p3-symmetric 0.4.3-succinct", "rand 0.8.6", "serde", ] [[package]] name = "p3-challenger" -version = "0.3.3-succinct" +version = "0.4.3-succinct" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75358edd6e2562752c01f5064a66d88144a3e75ace0407166dbdf8a727597f52" +checksum = "b6a908924d43e4cfb93fb41c8346cac211b70314385a9037e9241f5b7f3eaf77" dependencies = [ - "p3-field 0.3.3-succinct", - "p3-maybe-rayon 0.3.3-succinct", - "p3-symmetric 0.3.3-succinct", - "p3-util 0.3.3-succinct", + "p3-field 0.4.3-succinct", + "p3-maybe-rayon 0.4.3-succinct", + "p3-symmetric 0.4.3-succinct", + "p3-util 0.4.3-succinct", "serde", "tracing", ] @@ -5369,14 +5399,14 @@ dependencies = [ [[package]] name = "p3-dft" -version = "0.3.3-succinct" +version = "0.4.3-succinct" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "761f1e1b014f2b1b69bd0309124e233d64aa3590e6a41ee786000dd849506d51" +checksum = "be6408b10a2c27eb13a7d5580c546c2179a8dc7dbc10a990657311891f9b41c0" dependencies = [ - "p3-field 0.3.3-succinct", - "p3-matrix 0.3.3-succinct", - "p3-maybe-rayon 0.3.3-succinct", - "p3-util 0.3.3-succinct", + "p3-field 0.4.3-succinct", + "p3-matrix 0.4.3-succinct", + "p3-maybe-rayon 0.4.3-succinct", + "p3-util 0.4.3-succinct", "tracing", ] @@ -5396,14 +5426,14 @@ dependencies = [ [[package]] name = "p3-field" -version = "0.3.3-succinct" +version = "0.4.3-succinct" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2df7cebaa4079b24e0dd7e3aad59eebcbb99a67c1271f79ad884a7c032f5f183" +checksum = "3dc75969ca3ac847f43e632ab979d59ff7a68f9eac8dbf8edcbba47fc2e1d3aa" dependencies = [ "itertools 0.12.1", "num-bigint 0.4.6", "num-traits", - "p3-util 0.3.3-succinct", + "p3-util 0.4.3-succinct", "rand 0.8.6", "serde", ] @@ -5425,16 +5455,16 @@ dependencies = [ [[package]] name = "p3-koala-bear" -version = "0.3.3-succinct" +version = "0.4.3-succinct" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cea0ba3389b034b6088d566aea8b57aa29dd2e180966e0c8056f61331c92b4e" +checksum = "3a9683cd0ef68100df7c62490533047bcf19c04c4a0fa1efc9d7c1e03e31f6b3" dependencies = [ "cfg-if 1.0.4", "num-bigint 0.4.6", - "p3-field 0.3.3-succinct", - "p3-mds 0.3.3-succinct", - "p3-poseidon2 0.3.3-succinct", - "p3-symmetric 0.3.3-succinct", + "p3-field 0.4.3-succinct", + "p3-mds 0.4.3-succinct", + "p3-poseidon2 0.4.3-succinct", + "p3-symmetric 0.4.3-succinct", "rand 0.8.6", "rustc_version 0.4.1", "serde", @@ -5457,14 +5487,14 @@ dependencies = [ [[package]] name = "p3-matrix" -version = "0.3.3-succinct" +version = "0.4.3-succinct" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fae5cc6ce726cc265cc687c1214e3f1ac1f5c6e973442286ba00d1e75da1c3cb" +checksum = "75c3f150ceb90e09539413bf481e618d05ee19210b4e467d2902eb82d2e15281" dependencies = [ "itertools 0.12.1", - "p3-field 0.3.3-succinct", - "p3-maybe-rayon 0.3.3-succinct", - "p3-util 0.3.3-succinct", + "p3-field 0.4.3-succinct", + "p3-maybe-rayon 0.4.3-succinct", + "p3-util 0.4.3-succinct", "rand 0.8.6", "serde", "tracing", @@ -5486,9 +5516,9 @@ dependencies = [ [[package]] name = "p3-maybe-rayon" -version = "0.3.3-succinct" +version = "0.4.3-succinct" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ac1d2f102cf8c71dba1b449575c99697781fcc028831e83d2245787bd7a650" +checksum = "e0641952b42da45e1dfa2d4a2a3163e330f944ad9740942f35026c0a71a605f1" [[package]] name = "p3-maybe-rayon" @@ -5497,16 +5527,16 @@ source = "git+https://github.com/Plonky3/Plonky3.git#3f67d136c71bec40f180c85d0bb [[package]] name = "p3-mds" -version = "0.3.3-succinct" +version = "0.4.3-succinct" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f072643e385d65fb9eb089ee6824b320417f78671a0db748566e057e28b250e" +checksum = "aa4a5f250e174dcfca5cbeac6ad75713924e7e7320e0a335e3c50b8b1f4fe8ec" dependencies = [ "itertools 0.12.1", - "p3-dft 0.3.3-succinct", - "p3-field 0.3.3-succinct", - "p3-matrix 0.3.3-succinct", - "p3-symmetric 0.3.3-succinct", - "p3-util 0.3.3-succinct", + "p3-dft 0.4.3-succinct", + "p3-field 0.4.3-succinct", + "p3-matrix 0.4.3-succinct", + "p3-symmetric 0.4.3-succinct", + "p3-util 0.4.3-succinct", "rand 0.8.6", ] @@ -5557,14 +5587,14 @@ dependencies = [ [[package]] name = "p3-poseidon2" -version = "0.3.3-succinct" +version = "0.4.3-succinct" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00cc4b6e8a439f79541b0910a016da9e6e12a05a24309bbb713e1db0db396952" +checksum = "522986377b2164c5f94f2dae88e0e0a3d169cc6239202ef4aeb4322d60feffd0" dependencies = [ "gcd", - "p3-field 0.3.3-succinct", - "p3-mds 0.3.3-succinct", - "p3-symmetric 0.3.3-succinct", + "p3-field 0.4.3-succinct", + "p3-mds 0.4.3-succinct", + "p3-symmetric 0.4.3-succinct", "rand 0.8.6", "serde", ] @@ -5583,12 +5613,12 @@ dependencies = [ [[package]] name = "p3-symmetric" -version = "0.3.3-succinct" +version = "0.4.3-succinct" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eebff7fea7deb08a57ccf731a0ed39df25cc66a0e0c2d92c4472c4dee02ee21" +checksum = "9047ce85c086a9b3f118e10078f10636f7bfeed5da871a04da0b61400af8793a" dependencies = [ "itertools 0.12.1", - "p3-field 0.3.3-succinct", + "p3-field 0.4.3-succinct", "serde", ] @@ -5605,9 +5635,9 @@ dependencies = [ [[package]] name = "p3-util" -version = "0.3.3-succinct" +version = "0.4.3-succinct" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8164df89bbc92e29938f916cc5f1ccbfe6a36fb5040f21ba93c1f21985b9868" +checksum = "cff962f8eaa5f36e0447cee7c241f6b4b475fadf3ee61f154327a26bb4e009ba" dependencies = [ "serde", ] @@ -5707,7 +5737,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ - "base64", + "base64 0.22.1", "serde_core", ] @@ -6041,7 +6071,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" dependencies = [ "anyhow", - "itertools 0.12.1", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.117", @@ -6471,7 +6501,7 @@ version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-core", "http", @@ -6601,6 +6631,19 @@ dependencies = [ "rustc-hex", ] +[[package]] +name = "rlsf" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1646a59a9734b8b7a0ac51689388a60fe1625d4b956348e9de07591a1478457a" +dependencies = [ + "cfg-if 1.0.4", + "const-default", + "libc", + "rustversion", + "svgbobdoc", +] + [[package]] name = "rocksdb" version = "0.24.0" @@ -7105,20 +7148,20 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "slop-algebra" -version = "6.2.1" +version = "6.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a473c3a06b466dd0708829415a8a9fab451740da066e07862c8c098904aaad6" +checksum = "8c112cafd4c5c374d267a48c8976d12fca3b0f2cc2e44e6fb1343808310b4fc6" dependencies = [ "itertools 0.14.0", - "p3-field 0.3.3-succinct", + "p3-field 0.4.3-succinct", "serde", ] [[package]] name = "slop-bn254" -version = "6.2.1" +version = "6.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7fbae5dd16a3d1e87c9e99cfd557338171710be01458bd5b12dded3878d3fd8" +checksum = "ea991a652ea2e55f0523649f5c9efbfb32cf9fdcc2bf3b3580b4343a94379503" dependencies = [ "ff", "p3-bn254-fr", @@ -7131,12 +7174,12 @@ dependencies = [ [[package]] name = "slop-challenger" -version = "6.2.1" +version = "6.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4e80df718cef7d3100658dc8b46fafcc994b814421ec9a7d0763a6ee1e5070c" +checksum = "33b33f1eaadcd4159157758b0edcf1c14f470915f85c648e660e4740ff83e921" dependencies = [ "futures", - "p3-challenger 0.3.3-succinct", + "p3-challenger 0.4.3-succinct", "serde", "slop-algebra", "slop-symmetric", @@ -7144,12 +7187,12 @@ dependencies = [ [[package]] name = "slop-koala-bear" -version = "6.2.1" +version = "6.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6586b1c0e66c503e4026a8cb007349fa99c2466957c5b09d18fe658d1391ed8" +checksum = "8795ee9a92ecf0a604c0e3ed20e80bbc38772310c0622f94a2e1b66ca2455cb8" dependencies = [ "lazy_static", - "p3-koala-bear 0.3.3-succinct", + "p3-koala-bear 0.4.3-succinct", "serde", "slop-algebra", "slop-challenger", @@ -7159,29 +7202,29 @@ dependencies = [ [[package]] name = "slop-poseidon2" -version = "6.2.1" +version = "6.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c956b11fff1b8a071fa4ba982dc35e458cff1620dc7b33d9cf22d8df30895f79" +checksum = "25561d9ecf4bcf1aa2e800560ac557bff6dad943b9dc5cd0e8427fceca1e8c65" dependencies = [ - "p3-poseidon2 0.3.3-succinct", + "p3-poseidon2 0.4.3-succinct", ] [[package]] name = "slop-primitives" -version = "6.2.1" +version = "6.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de169e0ca381847f9efa0db5a54533371c10558d7aaed4cb3b2a9bae24a0fe83" +checksum = "f6b24ad70b49f40d6acfe7b1ccf042db25820abe305a4c2232f15204f63f0227" dependencies = [ "slop-algebra", ] [[package]] name = "slop-symmetric" -version = "6.2.1" +version = "6.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955145ad6e3a1d083a428f9274071cfbb44c3b29013aae9d6c4c29fb7328cfc0" +checksum = "94e580d03bb5383aca1c12579a8a24b838afb12eb356439e740cba34904b6864" dependencies = [ - "p3-symmetric 0.3.3-succinct", + "p3-symmetric 0.4.3-succinct", ] [[package]] @@ -7239,7 +7282,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures", "httparse", @@ -7250,9 +7293,9 @@ dependencies = [ [[package]] name = "sp1-lib" -version = "6.2.1" +version = "6.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02cd166e010c80e542585bf74585ea80eff117c361656cae43f2968cf0af12d4" +checksum = "629b673d6aa3c666b41e14d699323413fa90195c906365ba8c49d1b8e4531151" dependencies = [ "bincode", "serde", @@ -7261,9 +7304,9 @@ dependencies = [ [[package]] name = "sp1-primitives" -version = "6.2.1" +version = "6.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4df14efe799ebd675cf530c853153a4787327a2385067716dfad4ede79ff31ad" +checksum = "421b4590a89bf7c263f8a64844a4034ee8cb73c351498911ce0acd18da6b5ed2" dependencies = [ "bincode", "blake3", @@ -7283,6 +7326,25 @@ dependencies = [ "slop-symmetric", ] +[[package]] +name = "sp1-zkvm" +version = "6.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f18fbc79e5a1cc499d3ae3904771104a854493ea12c317ff1407137d86153c26" +dependencies = [ + "cfg-if 1.0.4", + "critical-section", + "embedded-alloc", + "getrandom 0.2.17", + "getrandom 0.3.4", + "lazy_static", + "libm", + "rand 0.8.6", + "sha2", + "sp1-lib", + "sp1-primitives", +] + [[package]] name = "sp1_bls12_381" version = "0.8.0-sp1-6.0.0" @@ -7460,6 +7522,19 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "svgbobdoc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2c04b93fc15d79b39c63218f15e3fdffaa4c227830686e3b7c5f41244eb3e50" +dependencies = [ + "base64 0.13.1", + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-width", +] + [[package]] name = "syn" version = "1.0.109" @@ -7564,7 +7639,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", @@ -8027,6 +8102,12 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -9062,6 +9143,15 @@ dependencies = [ "system-info", ] +[[package]] +name = "zkvm_guest_sp1" +version = "0.1.0" +dependencies = [ + "ethlambda-state-transition", + "ethlambda-types", + "sp1-zkvm", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index 0013defe..3c8ccb31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ "crates/common/crypto", "crates/common/metrics", "crates/common/test-fixtures", - "crates/common/types", + "crates/common/types", "crates/guest-program/sp1", "crates/net/api", "crates/net/p2p", "crates/net/rpc", diff --git a/crates/guest-program/sp1/Cargo.toml b/crates/guest-program/sp1/Cargo.toml new file mode 100644 index 00000000..bc1b7d3b --- /dev/null +++ b/crates/guest-program/sp1/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "zkvm_guest_sp1" +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] +sp1-zkvm = "6.1.0" +ethlambda-types.workspace = true +ethlambda-state-transition.workspace = true \ No newline at end of file diff --git a/crates/guest-program/sp1/src/main.rs b/crates/guest-program/sp1/src/main.rs new file mode 100644 index 00000000..1080d67d --- /dev/null +++ b/crates/guest-program/sp1/src/main.rs @@ -0,0 +1,13 @@ +#![no_main] +sp1_zkvm::entrypoint!(main); + +use ethlambda_state_transition::state_transition; +use ethlambda_types::{primitives::HashTreeRoot, stf::StfInput}; + +fn main() { + let input: StfInput = sp1_zkvm::io::read(); + let mut state = StfInput::return_state(input.clone()); + let block = &StfInput::return_block(input.clone()); + let _ = state_transition(&mut state, block); + sp1_zkvm::io::commit(&state.hash_tree_root()); +} From 4b61b80c2bd734e0b2d867297517fe899f9a834f Mon Sep 17 00:00:00 2001 From: Sahilgill24 Date: Wed, 22 Jul 2026 01:40:39 +0530 Subject: [PATCH 03/13] refactor: update guest program workspace and add public values --- Cargo.lock | 102 ++------------------------- Cargo.toml | 7 +- crates/common/types/src/stf.rs | 34 +++++++-- crates/guest-program/sp1/Cargo.toml | 20 +++--- crates/guest-program/sp1/src/main.rs | 28 ++++++-- 5 files changed, 72 insertions(+), 119 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b5c2a070..062a2edf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -574,7 +574,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" dependencies = [ - "base64 0.22.1", + "base64", "http", "log", "url", @@ -705,12 +705,6 @@ dependencies = [ "match-lookup", ] -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - [[package]] name = "base64" version = "0.22.1" @@ -1154,12 +1148,6 @@ dependencies = [ "crossbeam-utils 0.8.21", ] -[[package]] -name = "const-default" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b396d1f76d455557e1218ec8066ae14bba60b4b36ecd55577ba979f5db7ecaa" - [[package]] name = "const-hex" version = "1.19.0" @@ -1877,18 +1865,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "embedded-alloc" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f2de9133f68db0d4627ad69db767726c99ff8585272716708227008d3f1bddd" -dependencies = [ - "const-default", - "critical-section", - "linked_list_allocator", - "rlsf", -] - [[package]] name = "embedded-io" version = "0.4.0" @@ -3192,7 +3168,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-channel", "futures-util", @@ -4032,7 +4008,7 @@ source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=2f14d0ec9665a01 dependencies = [ "async-channel", "asynchronous-codec", - "base64 0.22.1", + "base64", "byteorder", "bytes", "either", @@ -4609,12 +4585,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "linked_list_allocator" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b23ac50abb8261cb38c6e2a7192d3302e0836dac1628f6a93b82b4fad185897" - [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -5737,7 +5707,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ - "base64 0.22.1", + "base64", "serde_core", ] @@ -6501,7 +6471,7 @@ version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-core", "http", @@ -6631,19 +6601,6 @@ dependencies = [ "rustc-hex", ] -[[package]] -name = "rlsf" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1646a59a9734b8b7a0ac51689388a60fe1625d4b956348e9de07591a1478457a" -dependencies = [ - "cfg-if 1.0.4", - "const-default", - "libc", - "rustversion", - "svgbobdoc", -] - [[package]] name = "rocksdb" version = "0.24.0" @@ -7282,7 +7239,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures", "httparse", @@ -7326,25 +7283,6 @@ dependencies = [ "slop-symmetric", ] -[[package]] -name = "sp1-zkvm" -version = "6.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f18fbc79e5a1cc499d3ae3904771104a854493ea12c317ff1407137d86153c26" -dependencies = [ - "cfg-if 1.0.4", - "critical-section", - "embedded-alloc", - "getrandom 0.2.17", - "getrandom 0.3.4", - "lazy_static", - "libm", - "rand 0.8.6", - "sha2", - "sp1-lib", - "sp1-primitives", -] - [[package]] name = "sp1_bls12_381" version = "0.8.0-sp1-6.0.0" @@ -7522,19 +7460,6 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" -[[package]] -name = "svgbobdoc" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2c04b93fc15d79b39c63218f15e3fdffaa4c227830686e3b7c5f41244eb3e50" -dependencies = [ - "base64 0.13.1", - "proc-macro2", - "quote", - "syn 1.0.109", - "unicode-width", -] - [[package]] name = "syn" version = "1.0.109" @@ -8102,12 +8027,6 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - [[package]] name = "unicode-xid" version = "0.2.6" @@ -9143,15 +9062,6 @@ dependencies = [ "system-info", ] -[[package]] -name = "zkvm_guest_sp1" -version = "0.1.0" -dependencies = [ - "ethlambda-state-transition", - "ethlambda-types", - "sp1-zkvm", -] - [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index 3c8ccb31..f0f3581c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,13 +9,18 @@ members = [ "crates/common/crypto", "crates/common/metrics", "crates/common/test-fixtures", - "crates/common/types", "crates/guest-program/sp1", + "crates/common/types", "crates/net/api", "crates/net/p2p", "crates/net/rpc", "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. +exclude = ["crates/guest-program/sp1"] + [workspace.package] authors = ["LambdaClass"] edition = "2024" diff --git a/crates/common/types/src/stf.rs b/crates/common/types/src/stf.rs index a1c8e053..f7d0aeb2 100644 --- a/crates/common/types/src/stf.rs +++ b/crates/common/types/src/stf.rs @@ -1,15 +1,37 @@ use libssz::{SszDecode, SszEncode}; use serde::{Deserialize, Serialize}; -use crate::{block::Block, state::State}; +use crate::{block::Block, primitives::H256, state::State}; -// we will pass as bytes, as we want this to be definitely serializable and both implement Sszencode and decode + +/// 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 { @@ -18,11 +40,11 @@ impl StfInput { } } - pub fn return_state(input: Self) -> State { - State::from_ssz_bytes(&input.state).expect("error decoding State") + pub fn state(&self) -> State { + State::from_ssz_bytes(&self.state).expect("error decoding State") } - pub fn return_block(input: Self) -> Block { - Block::from_ssz_bytes(&input.block).expect("error decoding block") + 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 index bc1b7d3b..1e2a93c7 100644 --- a/crates/guest-program/sp1/Cargo.toml +++ b/crates/guest-program/sp1/Cargo.toml @@ -1,15 +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" -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 +version = "0.1.0" +edition = "2024" +publish = false [dependencies] sp1-zkvm = "6.1.0" -ethlambda-types.workspace = true -ethlambda-state-transition.workspace = true \ No newline at end of file +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 index 1080d67d..859fb72a 100644 --- a/crates/guest-program/sp1/src/main.rs +++ b/crates/guest-program/sp1/src/main.rs @@ -2,12 +2,28 @@ sp1_zkvm::entrypoint!(main); use ethlambda_state_transition::state_transition; -use ethlambda_types::{primitives::HashTreeRoot, stf::StfInput}; +use ethlambda_types::{ + primitives::HashTreeRoot, + stf::{StfInput, StfPublicValues}, +}; fn main() { - let input: StfInput = sp1_zkvm::io::read(); - let mut state = StfInput::return_state(input.clone()); - let block = &StfInput::return_block(input.clone()); - let _ = state_transition(&mut state, block); - sp1_zkvm::io::commit(&state.hash_tree_root()); + 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); } From da701e1826f3ae3beefdc48aaf107d129ba58f49 Mon Sep 17 00:00:00 2001 From: Sahilgill24 Date: Wed, 22 Jul 2026 07:29:51 +0530 Subject: [PATCH 04/13] feat: setup the prover core for zkvm proving and verification --- crates/common/types/src/stf.rs | 7 ++--- crates/prover/core/Cargo.toml | 15 ++++++++++ crates/prover/core/src/lib.rs | 55 ++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 crates/prover/core/Cargo.toml create mode 100644 crates/prover/core/src/lib.rs diff --git a/crates/common/types/src/stf.rs b/crates/common/types/src/stf.rs index f7d0aeb2..4244b5e8 100644 --- a/crates/common/types/src/stf.rs +++ b/crates/common/types/src/stf.rs @@ -3,12 +3,11 @@ use serde::{Deserialize, Serialize}; use crate::{block::Block, primitives::H256, state::State}; - -/// Input type for the zkVM's -/// +/// 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. +/// wrapping pre-serialized SSZ bytes sidesteps that part. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StfInput { state: Vec, diff --git a/crates/prover/core/Cargo.toml b/crates/prover/core/Cargo.toml new file mode 100644 index 00000000..a8f3187d --- /dev/null +++ b/crates/prover/core/Cargo.toml @@ -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 + diff --git a/crates/prover/core/src/lib.rs b/crates/prover/core/src/lib.rs new file mode 100644 index 00000000..e0a7518c --- /dev/null +++ b/crates/prover/core/src/lib.rs @@ -0,0 +1,55 @@ +pub use ethlambda_types::stf::{StfInput, StfPublicValues}; + +/// 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; +} + +/// 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), +} From 97b2ad40836fb8e3a0863c7e24ff7cca58203156 Mon Sep 17 00:00:00 2001 From: Sahilgill24 Date: Wed, 22 Jul 2026 07:31:17 +0530 Subject: [PATCH 05/13] feat: setup the prover backend for sp1 --- Cargo.lock | 8 +++++ Cargo.toml | 21 ++++++++--- crates/prover/sp1/Cargo.toml | 20 +++++++++++ crates/prover/sp1/build.rs | 5 +++ crates/prover/sp1/src/lib.rs | 67 ++++++++++++++++++++++++++++++++++++ 5 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 crates/prover/sp1/Cargo.toml create mode 100644 crates/prover/sp1/build.rs create mode 100644 crates/prover/sp1/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 062a2edf..bae86cf1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2124,6 +2124,14 @@ dependencies = [ "tracing", ] +[[package]] +name = "ethlambda-prover-core" +version = "0.1.0" +dependencies = [ + "ethlambda-types", + "thiserror 2.0.18", +] + [[package]] name = "ethlambda-rpc" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index f0f3581c..58717778 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,13 +13,18 @@ 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 +# 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. -exclude = ["crates/guest-program/sp1"] +# +# `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"] @@ -69,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" @@ -103,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/prover/sp1/Cargo.toml b/crates/prover/sp1/Cargo.toml new file mode 100644 index 00000000..395223c3 --- /dev/null +++ b/crates/prover/sp1/Cargo.toml @@ -0,0 +1,20 @@ +# 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 + +[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" + +[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..9952bcb9 --- /dev/null +++ b/crates/prover/sp1/build.rs @@ -0,0 +1,5 @@ +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. + sp1_build::build_program("../../guest-program/sp1"); +} diff --git a/crates/prover/sp1/src/lib.rs b/crates/prover/sp1/src/lib.rs new file mode 100644 index 00000000..cb8cc688 --- /dev/null +++ b/crates/prover/sp1/src/lib.rs @@ -0,0 +1,67 @@ +use ethlambda_prover_core::{Proof, ProverError, StfInput, StfProver, StfPublicValues}; +use sp1_sdk::{ + MockProver, Prover, SP1ProofWithPublicValues, SP1ProvingKey, SP1Stdin, SP1VerifyingKey, + include_elf, +}; + + +const STATE_TRANSITION_ELF: &[u8] = 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; + let (pk, vk) = client.setup(STATE_TRANSITION_ELF).await; + 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()))?; + + self.client + .verify(&sp1_proof, &self.vk) + .await + .map_err(|err| ProverError::Verify(err.to_string()))?; + + // The guest committed `StfPublicValues` via `io::commit`; read it back. + Ok(sp1_proof.public_values.read::()) + } +} From 88ad3fba3160132ee7df4362db62847f56ad0448 Mon Sep 17 00:00:00 2001 From: Sahilgill24 Date: Sun, 26 Jul 2026 07:07:45 +0530 Subject: [PATCH 06/13] feat: add zkVM target for guest program and its dependencies --- crates/blockchain/state_transition/Cargo.toml | 8 ++- crates/blockchain/state_transition/src/lib.rs | 17 +++++++ .../state_transition/src/metrics.rs | 50 +++++++++++++++++-- 3 files changed, 69 insertions(+), 6 deletions(-) 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 403b170a..b0fd1faf 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; use ethlambda_types::{ @@ -8,8 +12,21 @@ 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; pub mod metrics; diff --git a/crates/blockchain/state_transition/src/metrics.rs b/crates/blockchain/state_transition/src/metrics.rs index 8df30909..4fa22fa0 100644 --- a/crates/blockchain/state_transition/src/metrics.rs +++ b/crates/blockchain/state_transition/src/metrics.rs @@ -1,9 +1,20 @@ //! Prometheus metrics for state transition. +//! +//! Gated on the target: on the zkVM guest (`target_os = "zkvm"`) the statics +//! are dropped and each function's body becomes a no-op, so the call sites in +//! `lib.rs` never change. +#[cfg(not(target_os = "zkvm"))] use std::sync::LazyLock; +#[cfg(not(target_os = "zkvm"))] use ethlambda_metrics::*; +/// No-op version of the struct imported from ethlambda_metrics for the zkVM guest. +#[cfg(target_os = "zkvm")] +pub struct TimingGuard; + +#[cfg(not(target_os = "zkvm"))] static LEAN_STATE_TRANSITION_SLOTS_PROCESSED_TOTAL: LazyLock = LazyLock::new(|| { register_int_counter!( "lean_state_transition_slots_processed_total", @@ -12,6 +23,7 @@ static LEAN_STATE_TRANSITION_SLOTS_PROCESSED_TOTAL: LazyLock = LazyL .unwrap() }); +#[cfg(not(target_os = "zkvm"))] static LEAN_STATE_TRANSITION_ATTESTATIONS_PROCESSED_TOTAL: LazyLock = LazyLock::new(|| { register_int_counter!( @@ -21,6 +33,7 @@ static LEAN_STATE_TRANSITION_ATTESTATIONS_PROCESSED_TOTAL: LazyLock .unwrap() }); +#[cfg(not(target_os = "zkvm"))] static LEAN_FINALIZATIONS_TOTAL: LazyLock = LazyLock::new(|| { register_int_counter_vec!( "lean_finalizations_total", @@ -32,19 +45,29 @@ static LEAN_FINALIZATIONS_TOTAL: LazyLock = LazyLock::new(|| { /// Increment the slots processed counter by the given amount. pub fn inc_slots_processed(count: u64) { + #[cfg(not(target_os = "zkvm"))] LEAN_STATE_TRANSITION_SLOTS_PROCESSED_TOTAL.inc_by(count); + #[cfg(target_os = "zkvm")] + let _ = count; } /// Increment the attestations processed counter by the given amount. pub fn inc_attestations_processed(count: u64) { + #[cfg(not(target_os = "zkvm"))] LEAN_STATE_TRANSITION_ATTESTATIONS_PROCESSED_TOTAL.inc_by(count); + #[cfg(target_os = "zkvm")] + let _ = count; } /// Increment the finalization counter with the given result. pub fn inc_finalizations(result: &str) { + #[cfg(not(target_os = "zkvm"))] LEAN_FINALIZATIONS_TOTAL.with_label_values(&[result]).inc(); + #[cfg(target_os = "zkvm")] + let _ = result; } +#[cfg(not(target_os = "zkvm"))] static LEAN_STATE_TRANSITION_TIME_SECONDS: LazyLock = LazyLock::new(|| { register_histogram!( "lean_state_transition_time_seconds", @@ -54,6 +77,7 @@ static LEAN_STATE_TRANSITION_TIME_SECONDS: LazyLock = LazyLock::new(| .unwrap() }); +#[cfg(not(target_os = "zkvm"))] static LEAN_STATE_TRANSITION_SLOTS_PROCESSING_TIME_SECONDS: LazyLock = LazyLock::new(|| { register_histogram!( @@ -64,6 +88,7 @@ static LEAN_STATE_TRANSITION_SLOTS_PROCESSING_TIME_SECONDS: LazyLock .unwrap() }); +#[cfg(not(target_os = "zkvm"))] static LEAN_STATE_TRANSITION_BLOCK_PROCESSING_TIME_SECONDS: LazyLock = LazyLock::new(|| { register_histogram!( @@ -74,6 +99,7 @@ static LEAN_STATE_TRANSITION_BLOCK_PROCESSING_TIME_SECONDS: LazyLock .unwrap() }); +#[cfg(not(target_os = "zkvm"))] static LEAN_STATE_TRANSITION_ATTESTATIONS_PROCESSING_TIME_SECONDS: LazyLock = LazyLock::new(|| { register_histogram!( @@ -86,20 +112,36 @@ static LEAN_STATE_TRANSITION_ATTESTATIONS_PROCESSING_TIME_SECONDS: LazyLock TimingGuard { - TimingGuard::new(&LEAN_STATE_TRANSITION_TIME_SECONDS) + #[cfg(not(target_os = "zkvm"))] + let guard = TimingGuard::new(&LEAN_STATE_TRANSITION_TIME_SECONDS); + #[cfg(target_os = "zkvm")] + let guard = TimingGuard; + guard } /// Start timing slots processing. Records duration when the guard is dropped. pub fn time_slots_processing() -> TimingGuard { - TimingGuard::new(&LEAN_STATE_TRANSITION_SLOTS_PROCESSING_TIME_SECONDS) + #[cfg(not(target_os = "zkvm"))] + let guard = TimingGuard::new(&LEAN_STATE_TRANSITION_SLOTS_PROCESSING_TIME_SECONDS); + #[cfg(target_os = "zkvm")] + let guard = TimingGuard; + guard } /// Start timing block processing. Records duration when the guard is dropped. pub fn time_block_processing() -> TimingGuard { - TimingGuard::new(&LEAN_STATE_TRANSITION_BLOCK_PROCESSING_TIME_SECONDS) + #[cfg(not(target_os = "zkvm"))] + let guard = TimingGuard::new(&LEAN_STATE_TRANSITION_BLOCK_PROCESSING_TIME_SECONDS); + #[cfg(target_os = "zkvm")] + let guard = TimingGuard; + guard } /// Start timing attestations processing. Records duration when the guard is dropped. pub fn time_attestations_processing() -> TimingGuard { - TimingGuard::new(&LEAN_STATE_TRANSITION_ATTESTATIONS_PROCESSING_TIME_SECONDS) + #[cfg(not(target_os = "zkvm"))] + let guard = TimingGuard::new(&LEAN_STATE_TRANSITION_ATTESTATIONS_PROCESSING_TIME_SECONDS); + #[cfg(target_os = "zkvm")] + let guard = TimingGuard; + guard } From f5b7ac6910e9ce6cb339b264d9738f0f321d5b23 Mon Sep 17 00:00:00 2001 From: Sahilgill24 Date: Tue, 28 Jul 2026 06:34:45 +0530 Subject: [PATCH 07/13] refactor: separate no-op module targetting zkvm for metrics --- crates/blockchain/state_transition/src/lib.rs | 9 ++- .../state_transition/src/metrics.rs | 81 +++++-------------- .../state_transition/src/metrics_noop.rs | 23 ++++++ 3 files changed, 51 insertions(+), 62 deletions(-) create mode 100644 crates/blockchain/state_transition/src/metrics_noop.rs diff --git a/crates/blockchain/state_transition/src/lib.rs b/crates/blockchain/state_transition/src/lib.rs index b0fd1faf..b6962955 100644 --- a/crates/blockchain/state_transition/src/lib.rs +++ b/crates/blockchain/state_transition/src/lib.rs @@ -15,7 +15,6 @@ use ethlambda_types::{ #[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")] @@ -28,6 +27,14 @@ macro_rules! warn { } 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 4fa22fa0..f0816d5b 100644 --- a/crates/blockchain/state_transition/src/metrics.rs +++ b/crates/blockchain/state_transition/src/metrics.rs @@ -1,20 +1,10 @@ //! Prometheus metrics for state transition. -//! -//! Gated on the target: on the zkVM guest (`target_os = "zkvm"`) the statics -//! are dropped and each function's body becomes a no-op, so the call sites in -//! `lib.rs` never change. -#[cfg(not(target_os = "zkvm"))] use std::sync::LazyLock; -#[cfg(not(target_os = "zkvm"))] +pub use ethlambda_metrics::TimingGuard; use ethlambda_metrics::*; -/// No-op version of the struct imported from ethlambda_metrics for the zkVM guest. -#[cfg(target_os = "zkvm")] -pub struct TimingGuard; - -#[cfg(not(target_os = "zkvm"))] static LEAN_STATE_TRANSITION_SLOTS_PROCESSED_TOTAL: LazyLock = LazyLock::new(|| { register_int_counter!( "lean_state_transition_slots_processed_total", @@ -23,7 +13,6 @@ static LEAN_STATE_TRANSITION_SLOTS_PROCESSED_TOTAL: LazyLock = LazyL .unwrap() }); -#[cfg(not(target_os = "zkvm"))] static LEAN_STATE_TRANSITION_ATTESTATIONS_PROCESSED_TOTAL: LazyLock = LazyLock::new(|| { register_int_counter!( @@ -33,7 +22,6 @@ static LEAN_STATE_TRANSITION_ATTESTATIONS_PROCESSED_TOTAL: LazyLock .unwrap() }); -#[cfg(not(target_os = "zkvm"))] static LEAN_FINALIZATIONS_TOTAL: LazyLock = LazyLock::new(|| { register_int_counter_vec!( "lean_finalizations_total", @@ -43,31 +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) { - #[cfg(not(target_os = "zkvm"))] - LEAN_STATE_TRANSITION_SLOTS_PROCESSED_TOTAL.inc_by(count); - #[cfg(target_os = "zkvm")] - let _ = count; -} - -/// Increment the attestations processed counter by the given amount. -pub fn inc_attestations_processed(count: u64) { - #[cfg(not(target_os = "zkvm"))] - LEAN_STATE_TRANSITION_ATTESTATIONS_PROCESSED_TOTAL.inc_by(count); - #[cfg(target_os = "zkvm")] - let _ = count; -} - -/// Increment the finalization counter with the given result. -pub fn inc_finalizations(result: &str) { - #[cfg(not(target_os = "zkvm"))] - LEAN_FINALIZATIONS_TOTAL.with_label_values(&[result]).inc(); - #[cfg(target_os = "zkvm")] - let _ = result; -} - -#[cfg(not(target_os = "zkvm"))] static LEAN_STATE_TRANSITION_TIME_SECONDS: LazyLock = LazyLock::new(|| { register_histogram!( "lean_state_transition_time_seconds", @@ -77,7 +40,6 @@ static LEAN_STATE_TRANSITION_TIME_SECONDS: LazyLock = LazyLock::new(| .unwrap() }); -#[cfg(not(target_os = "zkvm"))] static LEAN_STATE_TRANSITION_SLOTS_PROCESSING_TIME_SECONDS: LazyLock = LazyLock::new(|| { register_histogram!( @@ -88,7 +50,6 @@ static LEAN_STATE_TRANSITION_SLOTS_PROCESSING_TIME_SECONDS: LazyLock .unwrap() }); -#[cfg(not(target_os = "zkvm"))] static LEAN_STATE_TRANSITION_BLOCK_PROCESSING_TIME_SECONDS: LazyLock = LazyLock::new(|| { register_histogram!( @@ -99,7 +60,6 @@ static LEAN_STATE_TRANSITION_BLOCK_PROCESSING_TIME_SECONDS: LazyLock .unwrap() }); -#[cfg(not(target_os = "zkvm"))] static LEAN_STATE_TRANSITION_ATTESTATIONS_PROCESSING_TIME_SECONDS: LazyLock = LazyLock::new(|| { register_histogram!( @@ -110,38 +70,37 @@ static LEAN_STATE_TRANSITION_ATTESTATIONS_PROCESSING_TIME_SECONDS: LazyLock TimingGuard { - #[cfg(not(target_os = "zkvm"))] - let guard = TimingGuard::new(&LEAN_STATE_TRANSITION_TIME_SECONDS); - #[cfg(target_os = "zkvm")] - let guard = TimingGuard; - guard + TimingGuard::new(&LEAN_STATE_TRANSITION_TIME_SECONDS) } /// Start timing slots processing. Records duration when the guard is dropped. pub fn time_slots_processing() -> TimingGuard { - #[cfg(not(target_os = "zkvm"))] - let guard = TimingGuard::new(&LEAN_STATE_TRANSITION_SLOTS_PROCESSING_TIME_SECONDS); - #[cfg(target_os = "zkvm")] - let guard = TimingGuard; - guard + TimingGuard::new(&LEAN_STATE_TRANSITION_SLOTS_PROCESSING_TIME_SECONDS) } /// Start timing block processing. Records duration when the guard is dropped. pub fn time_block_processing() -> TimingGuard { - #[cfg(not(target_os = "zkvm"))] - let guard = TimingGuard::new(&LEAN_STATE_TRANSITION_BLOCK_PROCESSING_TIME_SECONDS); - #[cfg(target_os = "zkvm")] - let guard = TimingGuard; - guard + TimingGuard::new(&LEAN_STATE_TRANSITION_BLOCK_PROCESSING_TIME_SECONDS) } /// Start timing attestations processing. Records duration when the guard is dropped. pub fn time_attestations_processing() -> TimingGuard { - #[cfg(not(target_os = "zkvm"))] - let guard = TimingGuard::new(&LEAN_STATE_TRANSITION_ATTESTATIONS_PROCESSING_TIME_SECONDS); - #[cfg(target_os = "zkvm")] - let guard = TimingGuard; - guard + TimingGuard::new(&LEAN_STATE_TRANSITION_ATTESTATIONS_PROCESSING_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 +} From cb54b4e111aef91b9ac97f49a0aa63ce203d865d Mon Sep 17 00:00:00 2001 From: Sahilgill24 Date: Thu, 30 Jul 2026 06:35:02 +0530 Subject: [PATCH 08/13] feat: add the execute path to the Prover trait and sp1 prover --- crates/prover/core/src/lib.rs | 6 ++++++ crates/prover/sp1/Cargo.toml | 6 ++++++ crates/prover/sp1/build.rs | 11 ++++++++++- crates/prover/sp1/src/lib.rs | 36 ++++++++++++++++++++++++++++------- 4 files changed, 51 insertions(+), 8 deletions(-) diff --git a/crates/prover/core/src/lib.rs b/crates/prover/core/src/lib.rs index e0a7518c..4b34fbd7 100644 --- a/crates/prover/core/src/lib.rs +++ b/crates/prover/core/src/lib.rs @@ -38,6 +38,9 @@ pub trait StfProver { /// 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. @@ -52,4 +55,7 @@ pub enum ProverError { /// 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/sp1/Cargo.toml b/crates/prover/sp1/Cargo.toml index 395223c3..ef92c874 100644 --- a/crates/prover/sp1/Cargo.toml +++ b/crates/prover/sp1/Cargo.toml @@ -18,3 +18,9 @@ bincode = "1.3" [build-dependencies] sp1-build = "6.3.1" + +[dev-dependencies] +# Build valid `(state, block)` inputs for tests and cross-check the guest's +# committed roots against a host-side run of the same transition. +ethlambda-state-transition = { path = "../../blockchain/state_transition" } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } diff --git a/crates/prover/sp1/build.rs b/crates/prover/sp1/build.rs index 9952bcb9..e61913f7 100644 --- a/crates/prover/sp1/build.rs +++ b/crates/prover/sp1/build.rs @@ -1,5 +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. - sp1_build::build_program("../../guest-program/sp1"); + // + // `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/lib.rs b/crates/prover/sp1/src/lib.rs index cb8cc688..8d8af9b4 100644 --- a/crates/prover/sp1/src/lib.rs +++ b/crates/prover/sp1/src/lib.rs @@ -1,11 +1,10 @@ use ethlambda_prover_core::{Proof, ProverError, StfInput, StfProver, StfPublicValues}; use sp1_sdk::{ - MockProver, Prover, SP1ProofWithPublicValues, SP1ProvingKey, SP1Stdin, SP1VerifyingKey, - include_elf, + Elf, MockProver, ProveRequest, Prover, ProvingKey, SP1ProofWithPublicValues, SP1ProvingKey, + SP1Stdin, SP1VerifyingKey, include_elf, }; - -const STATE_TRANSITION_ELF: &[u8] = include_elf!("zkvm_guest_sp1"); +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. @@ -25,7 +24,13 @@ impl Sp1Prover { pub async fn new() -> Self { let client = MockProver::new().await; // let client = ProverClient::builder().cpu().await; - let (pk, vk) = client.setup(STATE_TRANSITION_ELF).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 } } } @@ -56,12 +61,29 @@ impl StfProver for Sp1Prover { 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) - .await + .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::()) + } } From 84e65dc387d8c3ec521fbc1435940c4d81cd1e55 Mon Sep 17 00:00:00 2001 From: Sahilgill24 Date: Thu, 30 Jul 2026 06:35:32 +0530 Subject: [PATCH 09/13] test: test the execute path for the sp1 prover --- crates/prover/sp1/tests/execute.rs | 72 ++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 crates/prover/sp1/tests/execute.rs diff --git a/crates/prover/sp1/tests/execute.rs b/crates/prover/sp1/tests/execute.rs new file mode 100644 index 00000000..a0aecdbc --- /dev/null +++ b/crates/prover/sp1/tests/execute.rs @@ -0,0 +1,72 @@ +//! Integration test for the SP1 STF prover's `execute` path. +//! +//! `execute` runs the guest in the SP1 RISC-V executor without proving, so it +//! is cheap enough for regular runs while still exercising the real guest +//! and the `StfInput`/`StfPublicValues` serialization/deserialization. + + +use ethlambda_prover_core::{StfInput, StfProver, StfPublicValues}; +use ethlambda_prover_sp1::Sp1Prover; +use ethlambda_state_transition::{process_block, process_slots}; +use ethlambda_types::{ + block::{Block, BlockBody}, + primitives::{H256, HashTreeRoot as _}, + state::{State, Validator}, +}; + +const GENESIS_TIME: u64 = 0; +const NUM_VALIDATORS: u64 = 4; + + +fn valid_transition() -> (StfInput, StfPublicValues) { + let validators = (0..NUM_VALIDATORS) + .map(|index| Validator { + attestation_pubkey: [0u8; 52], + proposal_pubkey: [0u8; 52], + index, + }) + .collect(); + let pre_state = State::from_genesis(GENESIS_TIME, validators); + + // Parent root as `process_slots` will leave it: the genesis header with its + // `state_root` filled in (mirrors block_builder.rs). + let mut parent_header = pre_state.latest_block_header.clone(); + parent_header.state_root = pre_state.hash_tree_root(); + let parent_root = parent_header.hash_tree_root(); + + let mut block = Block { + slot: 1, + proposer_index: 1 % NUM_VALIDATORS, + parent_root, + state_root: H256::ZERO, + body: BlockBody::default(), + }; + + let mut scratch = pre_state.clone(); + process_slots(&mut scratch, block.slot).expect("process_slots"); + process_block(&mut scratch, &block).expect("process_block"); + block.state_root = scratch.hash_tree_root(); + + let expected = StfPublicValues { + pre_state_root: pre_state.hash_tree_root(), + block_root: block.hash_tree_root(), + post_state_root: block.state_root, + }; + (StfInput::new(pre_state, block), expected) +} + +#[tokio::test] +async fn execute_commits_expected_roots() { + // generate the inputs and the output expected state. + let (input, expected) = valid_transition(); + + let prover = Sp1Prover::new().await; + let execution_result = prover.execute(&input).await.expect("execute failed"); + + assert_eq!(execution_result.pre_state_root, expected.pre_state_root, "pre_state_root"); + assert_eq!(execution_result.block_root, expected.block_root, "block_root"); + assert_eq!( + execution_result.post_state_root, expected.post_state_root, + "post_state_root" + ); +} From fc625d4cd3c73e4f3cfc229feee25da2c3c36e15 Mon Sep 17 00:00:00 2001 From: Sahilgill24 Date: Tue, 4 Aug 2026 14:55:37 +0530 Subject: [PATCH 10/13] feat: add mock chain driver for zkVM prover Backend --- crates/prover/core/Cargo.toml | 2 + crates/prover/core/src/lib.rs | 2 + crates/prover/core/src/mock_chain.rs | 115 +++++++++++++++++++++++++++ crates/prover/sp1/tests/execute.rs | 72 ----------------- 4 files changed, 119 insertions(+), 72 deletions(-) create mode 100644 crates/prover/core/src/mock_chain.rs delete mode 100644 crates/prover/sp1/tests/execute.rs diff --git a/crates/prover/core/Cargo.toml b/crates/prover/core/Cargo.toml index a8f3187d..0b0a4ac9 100644 --- a/crates/prover/core/Cargo.toml +++ b/crates/prover/core/Cargo.toml @@ -11,5 +11,7 @@ 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 index 4b34fbd7..11b43661 100644 --- a/crates/prover/core/src/lib.rs +++ b/crates/prover/core/src/lib.rs @@ -1,5 +1,7 @@ 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) 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/tests/execute.rs b/crates/prover/sp1/tests/execute.rs deleted file mode 100644 index a0aecdbc..00000000 --- a/crates/prover/sp1/tests/execute.rs +++ /dev/null @@ -1,72 +0,0 @@ -//! Integration test for the SP1 STF prover's `execute` path. -//! -//! `execute` runs the guest in the SP1 RISC-V executor without proving, so it -//! is cheap enough for regular runs while still exercising the real guest -//! and the `StfInput`/`StfPublicValues` serialization/deserialization. - - -use ethlambda_prover_core::{StfInput, StfProver, StfPublicValues}; -use ethlambda_prover_sp1::Sp1Prover; -use ethlambda_state_transition::{process_block, process_slots}; -use ethlambda_types::{ - block::{Block, BlockBody}, - primitives::{H256, HashTreeRoot as _}, - state::{State, Validator}, -}; - -const GENESIS_TIME: u64 = 0; -const NUM_VALIDATORS: u64 = 4; - - -fn valid_transition() -> (StfInput, StfPublicValues) { - let validators = (0..NUM_VALIDATORS) - .map(|index| Validator { - attestation_pubkey: [0u8; 52], - proposal_pubkey: [0u8; 52], - index, - }) - .collect(); - let pre_state = State::from_genesis(GENESIS_TIME, validators); - - // Parent root as `process_slots` will leave it: the genesis header with its - // `state_root` filled in (mirrors block_builder.rs). - let mut parent_header = pre_state.latest_block_header.clone(); - parent_header.state_root = pre_state.hash_tree_root(); - let parent_root = parent_header.hash_tree_root(); - - let mut block = Block { - slot: 1, - proposer_index: 1 % NUM_VALIDATORS, - parent_root, - state_root: H256::ZERO, - body: BlockBody::default(), - }; - - let mut scratch = pre_state.clone(); - process_slots(&mut scratch, block.slot).expect("process_slots"); - process_block(&mut scratch, &block).expect("process_block"); - block.state_root = scratch.hash_tree_root(); - - let expected = StfPublicValues { - pre_state_root: pre_state.hash_tree_root(), - block_root: block.hash_tree_root(), - post_state_root: block.state_root, - }; - (StfInput::new(pre_state, block), expected) -} - -#[tokio::test] -async fn execute_commits_expected_roots() { - // generate the inputs and the output expected state. - let (input, expected) = valid_transition(); - - let prover = Sp1Prover::new().await; - let execution_result = prover.execute(&input).await.expect("execute failed"); - - assert_eq!(execution_result.pre_state_root, expected.pre_state_root, "pre_state_root"); - assert_eq!(execution_result.block_root, expected.block_root, "block_root"); - assert_eq!( - execution_result.post_state_root, expected.post_state_root, - "post_state_root" - ); -} From f4575b46b80fdf861e851c627635f5c756bd399b Mon Sep 17 00:00:00 2001 From: Sahilgill24 Date: Tue, 4 Aug 2026 14:57:09 +0530 Subject: [PATCH 11/13] feat: add binary to run zkVM prover for the mock chain --- Cargo.lock | 1 + crates/prover/sp1/Cargo.toml | 13 +-- crates/prover/sp1/src/bin/prove.rs | 129 +++++++++++++++++++++++++++++ crates/prover/sp1/src/lib.rs | 9 +- 4 files changed, 141 insertions(+), 11 deletions(-) create mode 100644 crates/prover/sp1/src/bin/prove.rs diff --git a/Cargo.lock b/Cargo.lock index b013e3db..3d2d5473 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2128,6 +2128,7 @@ dependencies = [ name = "ethlambda-prover-core" version = "0.1.0" dependencies = [ + "ethlambda-state-transition", "ethlambda-types", "thiserror 2.0.18", ] diff --git a/crates/prover/sp1/Cargo.toml b/crates/prover/sp1/Cargo.toml index ef92c874..e74df7da 100644 --- a/crates/prover/sp1/Cargo.toml +++ b/crates/prover/sp1/Cargo.toml @@ -8,6 +8,10 @@ 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" } @@ -15,12 +19,9 @@ 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" - -[dev-dependencies] -# Build valid `(state, block)` inputs for tests and cross-check the guest's -# committed roots against a host-side run of the same transition. -ethlambda-state-transition = { path = "../../blockchain/state_transition" } -tokio = { version = "1", features = ["macros", "rt-multi-thread"] } 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 index 8d8af9b4..63eab1b7 100644 --- a/crates/prover/sp1/src/lib.rs +++ b/crates/prover/sp1/src/lib.rs @@ -11,7 +11,7 @@ const CYCLE_LIMIT: u64 = 10_000_000; /// /// 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 +/// [TODO!]: check if using Lazylock might be better pub struct Sp1Prover { client: MockProver, pk: SP1ProvingKey, @@ -20,7 +20,7 @@ pub struct Sp1Prover { impl Sp1Prover { /// Build the prover once, caching the proving/verifying keys and - /// currently uses the MockProver. + /// currently uses the MockProver. pub async fn new() -> Self { let client = MockProver::new().await; // let client = ProverClient::builder().cpu().await; @@ -37,7 +37,6 @@ impl Sp1Prover { impl StfProver for Sp1Prover { async fn prove(&self, input: &StfInput) -> Result { - let mut stdin = SP1Stdin::new(); stdin.write(input); @@ -52,8 +51,8 @@ impl StfProver for Sp1Prover { // 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()))?; + let bytes = bincode::serialize(&proof) + .map_err(|err| ProverError::Serialization(err.to_string()))?; Ok(Proof(bytes)) } From fe526dfca875ec9358f82d7bb30226ccee504e2a Mon Sep 17 00:00:00 2001 From: Sahilgill24 Date: Tue, 8 Sep 2026 04:10:20 +0530 Subject: [PATCH 12/13] feat: add SSZ byte fetching route from rpc for zkVM proving --- crates/net/rpc/src/base.rs | 2 +- crates/net/rpc/src/blocks.rs | 46 ++++++++++++++++++++++++++++++++++- crates/net/rpc/src/lib.rs | 2 +- crates/prover/core/src/lib.rs | 2 -- crates/prover/sp1/Cargo.toml | 7 ------ 5 files changed, 47 insertions(+), 12 deletions(-) diff --git a/crates/net/rpc/src/base.rs b/crates/net/rpc/src/base.rs index 0ccf47ed..f6b5c9b3 100644 --- a/crates/net/rpc/src/base.rs +++ b/crates/net/rpc/src/base.rs @@ -68,7 +68,7 @@ pub(crate) fn json_response(value: T) -> axum::response::Re response } -fn ssz_response(bytes: Vec) -> axum::response::Response { +pub(crate) fn ssz_response(bytes: Vec) -> axum::response::Response { let mut response = bytes.into_response(); response.headers_mut().insert( header::CONTENT_TYPE, diff --git a/crates/net/rpc/src/blocks.rs b/crates/net/rpc/src/blocks.rs index 9082b43d..5ecae39a 100644 --- a/crates/net/rpc/src/blocks.rs +++ b/crates/net/rpc/src/blocks.rs @@ -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 { 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. @@ -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, + State(store): State, +) -> 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, + State(store): State, +) -> 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 diff --git a/crates/net/rpc/src/lib.rs b/crates/net/rpc/src/lib.rs index 6674b0b7..b606fa4c 100644 --- a/crates/net/rpc/src/lib.rs +++ b/crates/net/rpc/src/lib.rs @@ -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 { diff --git a/crates/prover/core/src/lib.rs b/crates/prover/core/src/lib.rs index 11b43661..4b34fbd7 100644 --- a/crates/prover/core/src/lib.rs +++ b/crates/prover/core/src/lib.rs @@ -1,7 +1,5 @@ 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) diff --git a/crates/prover/sp1/Cargo.toml b/crates/prover/sp1/Cargo.toml index e74df7da..395223c3 100644 --- a/crates/prover/sp1/Cargo.toml +++ b/crates/prover/sp1/Cargo.toml @@ -8,10 +8,6 @@ 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" } @@ -19,9 +15,6 @@ 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" From dd212c20d8a47cd5f5eaf9bae7ca9cb3c8d051c7 Mon Sep 17 00:00:00 2001 From: Sahilgill24 Date: Tue, 8 Sep 2026 04:12:06 +0530 Subject: [PATCH 13/13] feat: add replay crate for actual zkVM proving on devnet --- crates/prover/core/Cargo.toml | 2 - crates/prover/core/src/mock_chain.rs | 115 --------------------------- tooling/replay/Cargo.toml | 31 ++++++++ tooling/replay/src/fetcher.rs | 71 +++++++++++++++++ tooling/replay/src/main.rs | 51 ++++++++++++ tooling/replay/src/run.rs | 105 ++++++++++++++++++++++++ 6 files changed, 258 insertions(+), 117 deletions(-) delete mode 100644 crates/prover/core/src/mock_chain.rs create mode 100644 tooling/replay/Cargo.toml create mode 100644 tooling/replay/src/fetcher.rs create mode 100644 tooling/replay/src/main.rs create mode 100644 tooling/replay/src/run.rs diff --git a/crates/prover/core/Cargo.toml b/crates/prover/core/Cargo.toml index 0b0a4ac9..a8f3187d 100644 --- a/crates/prover/core/Cargo.toml +++ b/crates/prover/core/Cargo.toml @@ -11,7 +11,5 @@ 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/mock_chain.rs b/crates/prover/core/src/mock_chain.rs deleted file mode 100644 index 2c526af4..00000000 --- a/crates/prover/core/src/mock_chain.rs +++ /dev/null @@ -1,115 +0,0 @@ -//! 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/tooling/replay/Cargo.toml b/tooling/replay/Cargo.toml new file mode 100644 index 00000000..a3674b3e --- /dev/null +++ b/tooling/replay/Cargo.toml @@ -0,0 +1,31 @@ +# Standalone workspace (like tooling/event-monitor and crates/prover/sp1): the +# `sp1` feature pulls the SP1 prover + toolchain, so this is kept out of the +# root workspace and built on its own. +[workspace] + +[package] +name = "ethlambda-replay" +version = "0.1.0" +edition = "2024" +publish = false + +[[bin]] +name = "replay" +path = "src/main.rs" + +[dependencies] +ethlambda-prover-core = { path = "../../crates/prover/core" } +ethlambda-types = { path = "../../crates/common/types" } +ethlambda-state-transition = { path = "../../crates/blockchain/state_transition" } +libssz = "0.3.0" +hex = "0.4" +clap = { version = "4", features = ["derive", "env"] } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } +eyre = "0.6" +ethlambda-prover-sp1 = { path = "../../crates/prover/sp1", optional = true } + +[features] +default = ["exec"] +exec = [] +sp1 = ["dep:ethlambda-prover-sp1"] diff --git a/tooling/replay/src/fetcher.rs b/tooling/replay/src/fetcher.rs new file mode 100644 index 00000000..6d15d1ef --- /dev/null +++ b/tooling/replay/src/fetcher.rs @@ -0,0 +1,71 @@ +//! Fetch real `(block, pre_state)` pairs from a running ethlambda node over RPC +//! and turn them into `StfInput`s to replay. + +use ethlambda_prover_core::{StfInput, StfPublicValues}; +use ethlambda_types::{block::Block, primitives::HashTreeRoot as _, state::State}; +use eyre::{Context, bail}; +use libssz::SszDecode as _; + +/// A single transition ready to replay: the guest input plus the roots a +/// correct run must commit (computed here from the fetched block/state). +pub struct ReplayInput { + pub id: String, + pub input: StfInput, + pub expected: StfPublicValues, +} + +/// Fetch and assemble one transition, addressed by slot or `0x`-prefixed root. +pub async fn fetch_transition( + client: &reqwest::Client, + rpc_url: &str, + block_id: &str, +) -> eyre::Result { + let block = fetch_block(client, rpc_url, block_id) + .await + .wrap_err_with(|| format!("fetching block {block_id}"))?; + + // Pre-state = parent's post-state. + let parent_hex = format!("0x{}", hex::encode(block.parent_root.0)); + let state = fetch_state(client, rpc_url, &parent_hex) + .await + .wrap_err_with(|| format!("fetching pre-state at parent {parent_hex}"))?; + + let expected = StfPublicValues { + pre_state_root: state.hash_tree_root(), + block_root: block.hash_tree_root(), + // `state_transition` asserts the computed post-state equals this. + post_state_root: block.state_root, + }; + + Ok(ReplayInput { + id: block_id.to_string(), + input: StfInput::new(state, block), + expected, + }) +} + +async fn fetch_block(client: &reqwest::Client, rpc_url: &str, id: &str) -> eyre::Result { + let bytes = get_ssz(client, &format!("{rpc_url}/lean/v0/blocks/{id}/ssz")).await?; + Block::from_ssz_bytes(&bytes).map_err(|e| eyre::eyre!("decoding block SSZ: {e:?}")) +} + +async fn fetch_state(client: &reqwest::Client, rpc_url: &str, id: &str) -> eyre::Result { + let bytes = get_ssz(client, &format!("{rpc_url}/lean/v0/states/{id}")).await?; + State::from_ssz_bytes(&bytes).map_err(|e| eyre::eyre!("decoding state SSZ: {e:?}")) +} + +async fn get_ssz(client: &reqwest::Client, url: &str) -> eyre::Result> { + let resp = client + .get(url) + .send() + .await + .wrap_err("HTTP request failed")?; + if !resp.status().is_success() { + bail!("GET {url} -> {}", resp.status()); + } + Ok(resp + .bytes() + .await + .wrap_err("reading response body")? + .to_vec()) +} diff --git a/tooling/replay/src/main.rs b/tooling/replay/src/main.rs new file mode 100644 index 00000000..5c4d1012 --- /dev/null +++ b/tooling/replay/src/main.rs @@ -0,0 +1,51 @@ +mod fetcher; +mod run; + +use clap::{Parser, Subcommand, ValueEnum}; + +/// Replay real ethlambda state transitions through the STF prover. +#[derive(Parser)] +#[command(name = "replay")] +struct Cli { + /// RPC URL of a running ethlambda node. + #[arg(long, env = "REPLAY_RPC_URL", default_value = "http://127.0.0.1:5052")] + rpc_url: String, + /// Guest path per transition (SP1 backend only; the exec backend always executes). + #[arg(long, value_enum, default_value_t = Action::Execute)] + action: Action, + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand)] +enum Command { + /// Replay a single block, addressed by slot or root. + Block { id: String }, + /// Replay an inclusive range of slots. + Blocks { from: u64, to: u64 }, +} + +#[derive(Clone, Copy, ValueEnum)] +enum Action { + Execute, + Prove, +} + +#[tokio::main] +async fn main() -> eyre::Result<()> { + let cli = Cli::parse(); + let client = reqwest::Client::new(); + + let ids: Vec = match &cli.command { + Command::Block { id } => vec![id.clone()], + Command::Blocks { from, to } => (*from..=*to).map(|slot| slot.to_string()).collect(), + }; + + println!("Fetching {} block(s) from {}", ids.len(), cli.rpc_url); + let mut transitions = Vec::with_capacity(ids.len()); + for id in &ids { + transitions.push(fetcher::fetch_transition(&client, &cli.rpc_url, id).await?); + } + + run::run(cli.action, &transitions).await +} diff --git a/tooling/replay/src/run.rs b/tooling/replay/src/run.rs new file mode 100644 index 00000000..c090ea95 --- /dev/null +++ b/tooling/replay/src/run.rs @@ -0,0 +1,105 @@ +use ethlambda_types::ShortRoot; + +use crate::Action; +use crate::fetcher::ReplayInput; + +fn print_header(t: &ReplayInput) { + println!("\n-- block {} --", t.id); + println!( + " pre_state_root : {}", + ShortRoot(&t.expected.pre_state_root.0) + ); + println!( + " block_root : {}", + ShortRoot(&t.expected.block_root.0) + ); + println!( + " post_state_root : {}", + ShortRoot(&t.expected.post_state_root.0) + ); +} + +/// Host baseline: run `state_transition` directly, no zkVM. `--action` is +/// irrelevant here (there is no proof to generate). +#[cfg(not(feature = "sp1"))] +pub async fn run(_action: Action, transitions: &[ReplayInput]) -> eyre::Result<()> { + use ethlambda_state_transition::state_transition; + use ethlambda_types::primitives::HashTreeRoot as _; + + println!("Backend: exec (host state_transition, no zkVM)"); + for t in transitions { + print_header(t); + let mut state = t.input.state(); + let block = t.input.block(); + state_transition(&mut state, &block) + .map_err(|e| eyre::eyre!("state_transition failed for {}: {e:?}", t.id))?; + assert_eq!( + state.hash_tree_root(), + t.expected.post_state_root, + "post_state_root mismatch at {}", + t.id + ); + println!("Execution Completed"); + } + println!("\nExecuted {} transition(s) on the host", transitions.len()); + Ok(()) +} + +/// SP1 backend: run each transition through the guest via `execute` or +/// `prove` + `verify`, checking committed roots against the host expectation. +#[cfg(feature = "sp1")] +pub async fn run(action: Action, transitions: &[ReplayInput]) -> eyre::Result<()> { + use ethlambda_prover_core::StfProver; + use ethlambda_prover_sp1::Sp1Prover; + + println!("Backend: sp1 (MockProver)"); + let prover = Sp1Prover::new().await; + for t in transitions { + print_header(t); + match action { + Action::Execute => { + let pv = prover + .execute(&t.input) + .await + .map_err(|e| eyre::eyre!("execute {}: {e}", t.id))?; + check(&pv, &t.expected, &t.id); + println!("Execution Completed"); + } + Action::Prove => { + let proof = prover + .prove(&t.input) + .await + .map_err(|e| eyre::eyre!("prove {}: {e}", t.id))?; + let pv = prover + .verify(&proof) + .await + .map_err(|e| eyre::eyre!("verify {}: {e}", t.id))?; + check(&pv, &t.expected, &t.id); + println!("Proof bytes ({})", proof.as_bytes().len()); + } + } + } + let verb = match action { + Action::Execute => "Executed", + Action::Prove => "Proved", + }; + println!("\n{verb} {} transition(s) via SP1", transitions.len()); + Ok(()) +} + +#[cfg(feature = "sp1")] +fn check( + pv: ðlambda_prover_core::StfPublicValues, + expected: ðlambda_prover_core::StfPublicValues, + id: &str, +) { + assert_eq!( + pv.pre_state_root, expected.pre_state_root, + "pre_state_root at {id}" + ); + assert_eq!(pv.block_root, expected.block_root, "block_root at {id}"); + assert_eq!( + pv.post_state_root, expected.post_state_root, + "post_state_root at {id}" + ); +}