From 1a906b053daaaae9bb46bf8f0506842dbf53a2a4 Mon Sep 17 00:00:00 2001 From: Lucas Fiegl Date: Mon, 3 Aug 2026 12:58:01 -0300 Subject: [PATCH 01/30] refactor(l1): gate parallel paths on rayon only --- cmd/ethrex/Cargo.toml | 7 ++- crates/blockchain/Cargo.toml | 4 +- crates/blockchain/blockchain.rs | 28 +++++------ crates/blockchain/prewarm.rs | 52 ++++++++++--------- crates/common/Cargo.toml | 2 +- crates/common/types/block.rs | 12 ++--- crates/common/types/eip8025_cell.rs | 2 +- crates/common/types/mod.rs | 2 +- crates/common/types/transaction.rs | 4 +- crates/guest-program/bin/sp1/Cargo.lock | 3 -- crates/vm/Cargo.toml | 4 +- crates/vm/backends/levm/mod.rs | 66 ++++++++++++------------- crates/vm/levm/Cargo.toml | 2 +- crates/vm/levm/src/db/gen_db.rs | 16 +++--- crates/vm/levm/src/db/mod.rs | 8 +-- test/Cargo.toml | 1 - test/tests/levm/bal_view_tests.rs | 4 +- 17 files changed, 108 insertions(+), 109 deletions(-) diff --git a/cmd/ethrex/Cargo.toml b/cmd/ethrex/Cargo.toml index 3b1ea31076d..c6522792ce4 100644 --- a/cmd/ethrex/Cargo.toml +++ b/cmd/ethrex/Cargo.toml @@ -80,13 +80,18 @@ path = "./lib.rs" [features] debug = ["ethrex-vm/debug"] -default = ["rocksdb", "c-kzg", "secp256k1", "metrics", "jemalloc", "dev"] +default = ["rocksdb", "c-kzg", "secp256k1", "metrics", "jemalloc", "dev", "rayon"] dev = ["dep:ethrex-dev"] secp256k1 = [ "ethrex-vm/secp256k1", "ethrex-common/secp256k1", "ethrex-blockchain/secp256k1", ] +rayon = [ + "ethrex-vm/rayon", + "ethrex-common/rayon", + "ethrex-blockchain/rayon", +] c-kzg = [ "ethrex-vm/c-kzg", "ethrex-common/c-kzg", diff --git a/crates/blockchain/Cargo.toml b/crates/blockchain/Cargo.toml index 32770c93f8a..da726d78f8f 100644 --- a/crates/blockchain/Cargo.toml +++ b/crates/blockchain/Cargo.toml @@ -46,9 +46,9 @@ tempfile.workspace = true path = "./blockchain.rs" [features] -default = ["secp256k1"] +default = ["secp256k1", "rayon"] rayon = ["ethrex-vm/rayon"] -secp256k1 = ["ethrex-common/secp256k1", "ethrex-vm/secp256k1", "rayon"] +secp256k1 = ["ethrex-common/secp256k1", "ethrex-vm/secp256k1"] c-kzg = ["ethrex-common/c-kzg", "ethrex-vm/c-kzg"] metrics = ["ethrex-metrics/transactions", "ethrex-metrics/metrics"] eip-8025 = ["ethrex-common/eip-8025", "ethrex-vm/eip-8025"] diff --git a/crates/blockchain/blockchain.rs b/crates/blockchain/blockchain.rs index 693b63f76ec..a727c324d45 100644 --- a/crates/blockchain/blockchain.rs +++ b/crates/blockchain/blockchain.rs @@ -55,7 +55,7 @@ pub mod vm; use ::tracing::{error, info, instrument, warn}; // Every `debug!` call site lives in the rayon warmer path, so the import is // unused in any configuration that compiles that path out. -#[cfg(all(feature = "rayon", not(feature = "eip-8025")))] +#[cfg(feature = "rayon")] use ::tracing::debug; use constants::{AMSTERDAM_MAX_INITCODE_SIZE, MAX_INITCODE_SIZE, POST_OSAKA_GAS_LIMIT_CAP}; use error::MempoolError; @@ -97,10 +97,10 @@ use ethrex_storage::{ }; use ethrex_trie::node::{BranchNode, ExtensionNode, LeafNode}; use ethrex_trie::{Nibbles, Node, NodeRef, Trie, TrieError, TrieLogger, TrieNode}; -#[cfg(all(feature = "rayon", not(feature = "eip-8025")))] +#[cfg(feature = "rayon")] use ethrex_vm::backends::BLOATED_BATCH_THRESHOLD; use ethrex_vm::backends::CachingDatabase; -#[cfg(all(feature = "rayon", not(feature = "eip-8025")))] +#[cfg(feature = "rayon")] use ethrex_vm::backends::levm::LEVM; use ethrex_vm::backends::levm::db::DatabaseLogger; use ethrex_vm::{BlockExecutionResult, DynVmDatabase, Evm, EvmError, VmDatabase}; @@ -140,7 +140,7 @@ const MAX_MEMPOOL_SIZE_DEFAULT: usize = 10_000; pub const DEFAULT_GAP_ADMIT_OCCUPANCY_THRESHOLD: u8 = 90; /// Merkle write set for the trie-node prefetch: written storage slots and changed accounts. -#[cfg(all(feature = "rayon", not(feature = "eip-8025")))] +#[cfg(feature = "rayon")] type TriePrefetchInput = (Vec<(Address, H256)>, Vec
); /// Background thread for dropping large tree structures off the critical path. @@ -776,7 +776,7 @@ impl Blockchain { // be recorded as state accesses the canonical execution never makes, // polluting the witness (e.g. `engine_newPayloadWithWitnessV5`), so // warming is skipped entirely when a witness is being collected. - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] if self.options.bal_prefetch_enabled && !collect_witness && let Some(bal_ref) = bal.as_ref() @@ -803,7 +803,7 @@ impl Blockchain { // `prefetch_trie_nodes` reads the trie-node CFs directly via // `backend.begin_read()`, bypassing the witness-recording caching layer, so // it cannot pollute the witness. - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] let trie_prefetch_input: Option = if self.options.bal_prefetch_enabled && let Some(updates) = optimistic_updates.as_ref() { @@ -824,17 +824,17 @@ impl Blockchain { }; // Each thread that captures `bal` needs its own Arc clone (cheap pointer bump). - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] let bal_warmer = bal.clone(); let (execution_result, merkleization_result, warmer_duration) = std::thread::scope( |s| -> Result<_, ChainError> { - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] let vm_type = vm.vm_type; let cancelled_ref = &cancelled; - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] let bal_prefetch_enabled = self.options.bal_prefetch_enabled; - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] let warm_handle = (!collect_witness) .then(|| { std::thread::Builder::new() @@ -900,7 +900,7 @@ impl Blockchain { // before the block returns, but it shares the trie-node cold reads with // the merkleizer at higher aggregate queue depth, so it completes // within the exec/merkle window rather than extending it. - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] let trie_prefetch_handle = match trie_prefetch_input { Some((slots, accounts)) => { let storage = &self.storage; @@ -1097,7 +1097,7 @@ impl Blockchain { "merkleization thread panicked".to_string(), )) }); - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] let warmer_duration = warm_handle .map(|handle| { handle @@ -1107,12 +1107,12 @@ impl Blockchain { .unwrap_or(Duration::ZERO) }) .unwrap_or(Duration::ZERO); - #[cfg(any(not(feature = "rayon"), feature = "eip-8025"))] + #[cfg(not(feature = "rayon"))] let warmer_duration = Duration::ZERO; // Best-effort prefetch: join so the scope's borrows end cleanly. // The warming result is discarded, but surface a panic so a failing // prefetch (e.g. a RocksDB error) is observable rather than silent. - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] if let Some(h) = trie_prefetch_handle && let Err(e) = h.join() { diff --git a/crates/blockchain/prewarm.rs b/crates/blockchain/prewarm.rs index 07162bfb6f6..3959e08c7b7 100644 --- a/crates/blockchain/prewarm.rs +++ b/crates/blockchain/prewarm.rs @@ -12,22 +12,22 @@ //! speculative execution results are discarded and never reach shared //! state, so a wrong prediction costs wasted I/O, never incorrect state. -#[cfg(all(feature = "rayon", not(feature = "eip-8025")))] +#[cfg(feature = "rayon")] use crate::PrewarmedEntry; use crate::{Blockchain, BlockchainType}; -#[cfg(all(feature = "rayon", not(feature = "eip-8025")))] +#[cfg(feature = "rayon")] use ethrex_common::H256; use ethrex_common::types::{BlockHeader, MempoolTransaction, Transaction}; use ethrex_common::{Address, U256}; -#[cfg(all(feature = "rayon", not(feature = "eip-8025")))] +#[cfg(feature = "rayon")] use ethrex_crypto::NativeCrypto; use rustc_hash::FxHashMap; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, mpsc}; -#[cfg(all(feature = "rayon", not(feature = "eip-8025")))] +#[cfg(feature = "rayon")] use std::time::{Duration, Instant}; use std::time::{SystemTime, UNIX_EPOCH}; -#[cfg(all(feature = "rayon", not(feature = "eip-8025")))] +#[cfg(feature = "rayon")] use tracing::info; use tracing::warn; @@ -37,7 +37,7 @@ const SLOT_DURATION_SECS: u64 = 12; /// Warm up to this multiple of the parent block's gas limit worth of /// mempool txs per warming snapshot (the initial pass and each delta). -#[cfg_attr(any(not(feature = "rayon"), feature = "eip-8025"), allow(dead_code))] +#[cfg_attr(not(feature = "rayon"), allow(dead_code))] const GAS_BUDGET_MULTIPLIER: u64 = 6; /// Keep warming this long past the slot boundary. The next block cannot arrive @@ -64,9 +64,9 @@ fn next_slot_deadline_unix(parent_timestamp: u64) -> u64 { /// and the group it belongs to is truncated there. Ordering fidelity does /// not matter for warming, only membership; senders are never interleaved. // This and the helpers below are only called from `run_pass`, which is -// compiled out when the rayon feature is disabled (or eip-8025 is active); +// compiled out when the rayon feature is disabled; // keep them compiled for the unit tests instead of cfg-ing them out. -#[cfg_attr(any(not(feature = "rayon"), feature = "eip-8025"), allow(dead_code))] +#[cfg_attr(not(feature = "rayon"), allow(dead_code))] fn select_warm_set( txs_by_sender: FxHashMap>, base_fee: Option, @@ -100,7 +100,7 @@ fn select_warm_set( /// their own predecessors), so warming them only inflates the warm volume. /// This is a per-pass (per-snapshot) cap with no cross-pass accounting — see /// [`cap_sender_depth`] — not a slot-level ceiling. -#[cfg_attr(any(not(feature = "rayon"), feature = "eip-8025"), allow(dead_code))] +#[cfg_attr(not(feature = "rayon"), allow(dead_code))] const MAX_WARMED_TXS_PER_SENDER_PER_PASS: usize = 16; /// Keeps only a sender's ready contiguous nonce prefix starting at @@ -110,7 +110,7 @@ const MAX_WARMED_TXS_PER_SENDER_PER_PASS: usize = 16; /// ascending (as `filter_transactions` returns them). Without this, the warm /// set is padded with non-ready txs that all fail the nonce check — wasted /// warming that never touches state. -#[cfg_attr(any(not(feature = "rayon"), feature = "eip-8025"), allow(dead_code))] +#[cfg_attr(not(feature = "rayon"), allow(dead_code))] fn trim_to_ready(txs: Vec, account_nonce: u64) -> Vec { let mut expected = account_nonce; let mut ready = Vec::new(); @@ -133,7 +133,7 @@ fn trim_to_ready(txs: Vec, account_nonce: u64) -> Vec>, db: &dyn ethrex_vm::backends::LevmDatabase, @@ -153,7 +153,7 @@ fn filter_ready( /// nonce order. Every pass warms a sender's pending prefix from nonce 0, so /// capping the per-snapshot group bounds the slot's per-sender depth with no /// cross-pass accounting; senders left empty are removed. -#[cfg_attr(any(not(feature = "rayon"), feature = "eip-8025"), allow(dead_code))] +#[cfg_attr(not(feature = "rayon"), allow(dead_code))] fn cap_sender_depth( mut txs_by_sender: FxHashMap>, cap: usize, @@ -166,9 +166,9 @@ fn cap_sender_depth( } // Fields are only read by `run_pass`, which is compiled out when the rayon -// feature is disabled (or eip-8025 is active); avoid a dead-code warning in +// feature is disabled; avoid a dead-code warning in // that configuration, where the fields are still written by `trigger`. -#[cfg_attr(any(not(feature = "rayon"), feature = "eip-8025"), allow(dead_code))] +#[cfg_attr(not(feature = "rayon"), allow(dead_code))] struct PrewarmRequest { parent_header: BlockHeader, cancel: Arc, @@ -183,7 +183,7 @@ pub struct PrewarmHandle { current_cancel: Mutex>, } -#[cfg_attr(any(not(feature = "rayon"), feature = "eip-8025"), allow(dead_code))] +#[cfg_attr(not(feature = "rayon"), allow(dead_code))] fn unix_now() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) @@ -221,20 +221,18 @@ pub struct MempoolPrewarmer; impl MempoolPrewarmer { /// Spawns the prewarmer worker. Prewarming is L1-only: returns `None` - /// silently on L2. Returns `None` with a warn when the build lacks rayon - /// (or has eip-8025 active), or the pool/worker-thread creation fails. + /// silently on L2. Returns `None` with a warn when the build lacks rayon or + /// the pool/worker-thread creation fails. pub fn spawn(blockchain: Arc) -> Option { if !matches!(blockchain.options.r#type, BlockchainType::L1) { return None; } - #[cfg(any(not(feature = "rayon"), feature = "eip-8025"))] + #[cfg(not(feature = "rayon"))] { - warn!( - "Mempool prewarm requires the rayon feature and is unavailable on eip-8025 builds; disabled" - ); + warn!("Mempool prewarm requires the rayon feature; disabled"); None } - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] { // Half the available cores: plenty for warming while leaving // headroom for the rest of the node during the idle window. @@ -278,7 +276,7 @@ impl MempoolPrewarmer { } } -#[cfg(all(feature = "rayon", not(feature = "eip-8025")))] +#[cfg(feature = "rayon")] fn run_pass(blockchain: &Blockchain, pool: &rayon::ThreadPool, req: PrewarmRequest) { use crate::mempool::PendingTxFilter; use crate::vm::StoreVmDatabase; @@ -489,7 +487,7 @@ fn run_pass(blockchain: &Blockchain, pool: &rayon::ThreadPool, req: PrewarmReque /// proof walks pull them into the RocksDB block cache during the idle window. /// Proof outputs are discarded — the reads are the product. Returns the /// number of newly walked paths. -#[cfg(all(feature = "rayon", not(feature = "eip-8025")))] +#[cfg(feature = "rayon")] fn warm_merkle_paths( blockchain: &Blockchain, parent: &BlockHeader, @@ -685,13 +683,13 @@ mod tests { // A `Database` whose `get_account_state` succeeds for one sender (returning // a fixed nonce) and fails for everyone else, to exercise `filter_ready`'s // read-error branch. Every other method is unreachable from `filter_ready`. - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] struct OneReadableSenderDb { ok_sender: Address, nonce: u64, } - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] impl ethrex_levm::db::Database for OneReadableSenderDb { fn get_account_state( &self, @@ -741,7 +739,7 @@ mod tests { } } - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] #[test] fn filter_ready_trims_readable_and_keeps_unreadable_sender() { // Readable sender at account nonce 5: stale (4) dropped, ready (5,6) kept. diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index b3140575328..9e8466cf807 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -53,7 +53,7 @@ libssz-derive = { workspace = true } default = ["secp256k1", "rayon"] c-kzg = ["ethrex-crypto/c-kzg"] rayon = ["dep:rayon"] -secp256k1 = ["dep:secp256k1", "ethrex-crypto/secp256k1", "rayon"] +secp256k1 = ["dep:secp256k1", "ethrex-crypto/secp256k1"] eip-8025 = ["ethrex-trie/eip-8025"] risc0 = ["ethrex-crypto/risc0"] diff --git a/crates/common/types/block.rs b/crates/common/types/block.rs index a48175942c6..afaa303079f 100644 --- a/crates/common/types/block.rs +++ b/crates/common/types/block.rs @@ -17,7 +17,7 @@ use ethrex_rlp::{ structs::{Decoder, Encoder}, }; use ethrex_trie::Trie; -#[cfg(all(not(feature = "eip-8025"), feature = "rayon"))] +#[cfg(feature = "rayon")] use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use rkyv::{Archive, Deserialize as RDeserialize, Serialize as RSerialize}; use serde::{Deserialize, Serialize}; @@ -27,9 +27,9 @@ use std::cmp::{Ordering, max}; pub type BlockNumber = u64; pub type BlockHash = H256; -#[cfg(all(feature = "eip-8025", target_arch = "riscv64"))] +#[cfg(all(feature = "zisk", target_arch = "riscv64"))] use super::eip8025_cell::OnceCell; -#[cfg(not(all(feature = "eip-8025", target_arch = "riscv64")))] +#[cfg(not(all(feature = "zisk", target_arch = "riscv64")))] use once_cell::sync::OnceCell; #[derive( @@ -339,15 +339,15 @@ impl BlockBody { ) -> Result, CryptoError> { // Recovering addresses is computationally expensive. // Computing them in parallel greatly reduces execution time. - // In eip-8025 builds, use sequential iteration - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + // Without rayon, use sequential iteration + #[cfg(feature = "rayon")] return self .transactions .par_iter() .map(|tx| Ok((tx, tx.sender(crypto)?))) .collect::, CryptoError>>(); - #[cfg(any(feature = "eip-8025", not(feature = "rayon")))] + #[cfg(not(feature = "rayon"))] self.transactions .iter() .map(|tx| Ok((tx, tx.sender(crypto)?))) diff --git a/crates/common/types/eip8025_cell.rs b/crates/common/types/eip8025_cell.rs index 025cf28fc7c..4a1ce78180d 100644 --- a/crates/common/types/eip8025_cell.rs +++ b/crates/common/types/eip8025_cell.rs @@ -1,4 +1,4 @@ -/// `OnceCell` replacement for zkVM guest gated on `eip-8025` feature. +/// `OnceCell` replacement for the ZisK zkVM guest (riscv64). /// /// `once_cell::sync::OnceCell` atomics are pure overhead in zkVM guest. /// This struct copies the methods from `once_cell::unsync::OnceCell` and uses unsafe diff --git a/crates/common/types/mod.rs b/crates/common/types/mod.rs index 6fcd134ea2e..4126b5a090b 100644 --- a/crates/common/types/mod.rs +++ b/crates/common/types/mod.rs @@ -5,7 +5,7 @@ mod block; pub mod block_access_list; pub mod block_execution_witness; mod constants; -#[cfg(all(feature = "eip-8025", target_arch = "riscv64"))] +#[cfg(all(feature = "zisk", target_arch = "riscv64"))] pub(crate) mod eip8025_cell; #[cfg(feature = "eip-8025")] pub mod eip8025_ssz; diff --git a/crates/common/types/transaction.rs b/crates/common/types/transaction.rs index 35b4e6f8506..447f6a13b84 100644 --- a/crates/common/types/transaction.rs +++ b/crates/common/types/transaction.rs @@ -47,12 +47,12 @@ use ethrex_rlp::{ structs::{Decoder, Encoder}, }; -#[cfg(all(feature = "eip-8025", target_arch = "riscv64"))] +#[cfg(all(feature = "zisk", target_arch = "riscv64"))] use super::eip8025_cell::OnceCell; use crate::types::{ AccessList, AuthorizationList, BlobsBundle, constants::VERSIONED_HASH_VERSION_KZG, }; -#[cfg(not(all(feature = "eip-8025", target_arch = "riscv64")))] +#[cfg(not(all(feature = "zisk", target_arch = "riscv64")))] use once_cell::sync::OnceCell; // The `#[serde(untagged)]` attribute allows the `Transaction` enum to be serialized without diff --git a/crates/guest-program/bin/sp1/Cargo.lock b/crates/guest-program/bin/sp1/Cargo.lock index 0ba57d58337..d76db3bb816 100644 --- a/crates/guest-program/bin/sp1/Cargo.lock +++ b/crates/guest-program/bin/sp1/Cargo.lock @@ -779,7 +779,6 @@ dependencies = [ "libssz-types", "lru", "once_cell", - "rayon", "rkyv", "rustc-hash", "secp256k1", @@ -876,7 +875,6 @@ dependencies = [ "ethrex-rlp", "libssz", "malachite", - "rayon", "rustc-hash", "serde", "strum", @@ -922,7 +920,6 @@ dependencies = [ "ethrex-crypto", "ethrex-levm", "ethrex-rlp", - "rayon", "rustc-hash", "serde", "thiserror", diff --git a/crates/vm/Cargo.toml b/crates/vm/Cargo.toml index 88cb292938a..f5586cd4155 100644 --- a/crates/vm/Cargo.toml +++ b/crates/vm/Cargo.toml @@ -32,10 +32,10 @@ k256 = { workspace = true } path = "./lib.rs" [features] -default = ["secp256k1"] +default = ["secp256k1", "rayon"] rayon = ["dep:rayon", "ethrex-levm/rayon"] -secp256k1 = ["ethrex-levm/secp256k1", "ethrex-common/secp256k1", "rayon"] +secp256k1 = ["ethrex-levm/secp256k1", "ethrex-common/secp256k1"] c-kzg = ["ethrex-levm/c-kzg", "ethrex-common/c-kzg"] eip-8025 = ["ethrex-levm/eip-8025", "ethrex-common/eip-8025"] diff --git a/crates/vm/backends/levm/mod.rs b/crates/vm/backends/levm/mod.rs index aa516c30e11..1b2c0854230 100644 --- a/crates/vm/backends/levm/mod.rs +++ b/crates/vm/backends/levm/mod.rs @@ -11,13 +11,13 @@ use crate::system_contracts::{ use crate::{EvmError, ExecutionResult}; use bytes::Bytes; use ethrex_common::H256; -#[cfg(all(feature = "rayon", not(feature = "eip-8025")))] +#[cfg(feature = "rayon")] use ethrex_common::constants::EMPTY_KECCAK_HASH; use ethrex_common::types::Code; -#[cfg(all(feature = "rayon", not(feature = "eip-8025")))] +#[cfg(feature = "rayon")] use ethrex_common::types::TxType; use ethrex_common::types::block_access_list::BlockAccessList; -#[cfg(all(feature = "rayon", not(feature = "eip-8025")))] +#[cfg(feature = "rayon")] use ethrex_common::types::block_access_list::{ BalAddressIndex, find_exact_change_balance, find_exact_change_code, find_exact_change_nonce, find_exact_change_storage, has_exact_change_balance, has_exact_change_code, @@ -25,7 +25,7 @@ use ethrex_common::types::block_access_list::{ }; use ethrex_common::types::fee_config::FeeConfig; use ethrex_common::types::{AuthorizationTuple, EIP7702Transaction}; -#[cfg(all(feature = "rayon", not(feature = "eip-8025")))] +#[cfg(feature = "rayon")] use ethrex_common::utils::u256_from_big_endian_const; use ethrex_common::{ Address, U256, @@ -35,23 +35,23 @@ use ethrex_common::{ Withdrawal, requests::Requests, }, }; -#[cfg(all(feature = "rayon", not(feature = "eip-8025")))] +#[cfg(feature = "rayon")] use ethrex_common::{BigEndianHash, validate_block_access_list_size, validate_header_bal_indices}; use ethrex_crypto::Crypto; use ethrex_levm::EVMConfig; use ethrex_levm::StatelessValidator; -#[cfg(all(feature = "rayon", not(feature = "eip-8025")))] +#[cfg(feature = "rayon")] use ethrex_levm::account::{AccountStatus, LevmAccount}; use ethrex_levm::call_frame::Stack; use ethrex_levm::constants::{ POST_OSAKA_GAS_LIMIT_CAP, STACK_LIMIT, SYS_CALL_GAS_LIMIT, TX_MAX_GAS_LIMIT_AMSTERDAM, }; use ethrex_levm::db::gen_db::GeneralizedDatabase; -#[cfg(all(feature = "rayon", not(feature = "eip-8025")))] +#[cfg(feature = "rayon")] use ethrex_levm::db::gen_db::{ LazyBalCursor, code_from_bal, post_value_at_or_before, seed_one_address_info_from_bal, }; -#[cfg(all(feature = "rayon", not(feature = "eip-8025")))] +#[cfg(feature = "rayon")] use ethrex_levm::db::{Database, gen_db::CacheDB}; use ethrex_levm::errors::{InternalError, TxValidationError}; use ethrex_levm::memory::Memory; @@ -66,13 +66,13 @@ use ethrex_levm::{ errors::{ExecutionReport, TxResult, VMError}, vm::VM, }; -#[cfg(all(feature = "rayon", not(feature = "eip-8025")))] +#[cfg(feature = "rayon")] use rayon::iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator}; -#[cfg(all(feature = "rayon", not(feature = "eip-8025")))] +#[cfg(feature = "rayon")] use rustc_hash::{FxHashMap, FxHashSet}; use std::cmp::min; use std::sync::Arc; -#[cfg(all(feature = "rayon", not(feature = "eip-8025")))] +#[cfg(feature = "rayon")] use std::sync::atomic::AtomicBool; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::mpsc::Sender; @@ -203,7 +203,7 @@ pub fn check_2d_gas_allowance( /// /// Public so [`LEVM::validate_tx_execution`] is directly callable (and its /// error variants inspectable) from unit tests outside this crate. -#[cfg(all(feature = "rayon", not(feature = "eip-8025")))] +#[cfg(feature = "rayon")] #[derive(Debug, thiserror::Error)] pub enum BalValidationError { #[error("{0}")] @@ -478,11 +478,11 @@ impl LEVM { EvmError::Transaction(format!("Couldn't recover addresses with error: {error}")) })?; - #[cfg(any(feature = "eip-8025", not(feature = "rayon")))] - // `eip-8025` does not call `execute_block_pipeline` it uses - // `execute_block` instead. Adding dummy let to avoid unused warnings. + #[cfg(not(feature = "rayon"))] + // Without rayon there is no parallel BAL path, so these are unused. + // Adding dummy let to avoid unused warnings. let _ = (header_bal, bal_parallel_exec_enabled); - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] // When BAL is provided (Amsterdam+ validation path): use parallel execution. // The `is_amsterdam` gate is required: `execute_block_parallel` (and the // optimistic merkleization it feeds) is only correct on Amsterdam+; a @@ -896,7 +896,7 @@ impl LEVM { /// For each account in the BAL, extracts the **final** post-block state /// (highest `block_access_index` entry per field) and builds an AccountUpdate. /// State comes entirely from the BAL — no execution needed. - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] fn bal_to_account_updates( bal: &BlockAccessList, store: &dyn Database, @@ -1040,7 +1040,7 @@ impl LEVM { /// `max_idx` is the BAL block_access_index of the last tx whose effects /// should be visible. BAL indexing: 0 = system calls, 1 = tx 0, 2 = tx 1, ... /// For tx at index `i`, pass `max_idx = i` (diffs with index <= i = system + txs 0..i-1). - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] fn seed_db_from_bal( db: &mut GeneralizedDatabase, bal: &BlockAccessList, @@ -1089,7 +1089,7 @@ impl LEVM { /// Each tx runs independently on its own database pre-seeded with BAL /// intermediate state (geth-style). State for the merkleizer comes from /// `bal_to_account_updates`, not from tx execution. - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] #[allow(clippy::too_many_arguments, clippy::type_complexity)] fn execute_block_parallel( block: &Block, @@ -1557,7 +1557,7 @@ impl LEVM { /// Gets the seeded balance for an account at `seed_idx` from BAL, falling /// back to system_seed/store if no BAL entry exists before that index. - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] fn seeded_balance( seed_idx: u32, acct: ðrex_common::types::block_access_list::AccountChanges, @@ -1586,7 +1586,7 @@ impl LEVM { /// Gets the seeded code hash for an account at `seed_idx` from BAL, falling /// back to system_seed/store if no BAL entry exists before that index. - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] fn seeded_code_hash( seed_idx: u32, acct: ðrex_common::types::block_access_list::AccountChanges, @@ -1620,7 +1620,7 @@ impl LEVM { /// Gets the seeded nonce for an account at `seed_idx` from BAL, falling /// back to system_seed/store if no BAL entry exists before that index. - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] fn seeded_nonce( seed_idx: u32, acct: ðrex_common::types::block_access_list::AccountChanges, @@ -1654,7 +1654,7 @@ impl LEVM { /// `seeded_nonce` + `seeded_code_hash`, but reads the store at most once — the /// PART A no-op checks need all three for the same account, and an account /// with no BAL history before this tx would otherwise read it three times. - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] fn seeded_account_triple( seed_idx: u32, acct: ðrex_common::types::block_access_list::AccountChanges, @@ -1713,7 +1713,7 @@ impl LEVM { /// the recorder's `tx_initial` fast-path (a slot the EVM genuinely wrote this /// tx), else the pre-tx state (system_seed, then store). Used by the /// execution->BAL check for a slot absent from `storage_changes`. - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] fn seeded_storage_pre_value( addr: Address, key: H256, @@ -1730,7 +1730,7 @@ impl LEVM { /// Pre-tx value for a slot from the in-memory snapshot, falling back to the /// store. Shared tail of the two `seeded_storage*` helpers. - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] fn storage_from_seed_or_store( addr: Address, key: H256, @@ -1759,7 +1759,7 @@ impl LEVM { /// Fast path: for a slot the EVM genuinely wrote this tx, `tx_initial` already /// holds the start-of-tx value it captured during execution — identical to what /// this function would otherwise recompute — so return it and skip the lookup. - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] fn seeded_storage( seed_idx: u32, sc: ðrex_common::types::block_access_list::SlotChange, @@ -1809,7 +1809,7 @@ impl LEVM { /// Exposed as `pub` (rather than crate-private) solely so the direct /// `validate_tx_execution` unit tests in the `ethrex-test` crate /// (`test/tests/blockchain/bal_validate_tx_execution_tests.rs`) can call it. - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] #[allow(clippy::too_many_arguments)] pub fn validate_tx_execution( bal_idx: u32, @@ -2266,7 +2266,7 @@ impl LEVM { /// malicious builder could omit a withdrawal recipient from the BAL, /// causing the BAL-derived state root to exclude the withdrawal balance /// change. - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] fn validate_bal_withdrawal_index( db: &GeneralizedDatabase, bal: &BlockAccessList, @@ -2572,7 +2572,7 @@ impl LEVM { /// state, or whose value equals the pre-block value (a no-op), is rejected. /// Omissions and genuine divergences change the state root and are already /// caught by `validate_state_root`; the no-op case is the one this closes. - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] fn validate_bal_pre_exec_index( db: &GeneralizedDatabase, bal: &BlockAccessList, @@ -2727,7 +2727,7 @@ impl LEVM { /// The `store` parameter should be a `CachingDatabase`-wrapped store so that /// parallel workers can benefit from shared caching. The same cache should /// be used by the sequential execution phase. - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] pub fn warm_block( block: &Block, store: Arc, @@ -2781,7 +2781,7 @@ impl LEVM { /// transaction, so cancellation latency is bounded by one transaction's /// execution. Execution results are discarded — only cache population /// matters. - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] pub fn warm_txs( txs_with_sender: &[(&Transaction, Address)], header: &BlockHeader, @@ -2848,7 +2848,7 @@ impl LEVM { /// Flattened (address, slot) storage worklist for a BAL, in natural account /// order (slots grouped per account for storage-trie locality). - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] pub fn bal_storage_slots(bal: &BlockAccessList) -> Vec<(Address, H256)> { bal.accounts() .iter() @@ -2867,7 +2867,7 @@ impl LEVM { /// call site in `blockchain.rs`); warming them concurrently here let the /// executor race the warmer to the trie for SSTORE original values and cost /// ~22% of CPU. Keep storage warming synchronous and up front. - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] pub fn warm_block_from_bal( bal: &BlockAccessList, store: Arc, diff --git a/crates/vm/levm/Cargo.toml b/crates/vm/levm/Cargo.toml index 43b09d50351..787104d7743 100644 --- a/crates/vm/levm/Cargo.toml +++ b/crates/vm/levm/Cargo.toml @@ -43,7 +43,7 @@ sp1 = [] risc0 = ["c-kzg"] zisk = [] rayon = ["dep:rayon"] -secp256k1 = ["rayon"] +secp256k1 = [] [lints.rust] unsafe_code = "warn" diff --git a/crates/vm/levm/src/db/gen_db.rs b/crates/vm/levm/src/db/gen_db.rs index dac556b0880..bdb35182c82 100644 --- a/crates/vm/levm/src/db/gen_db.rs +++ b/crates/vm/levm/src/db/gen_db.rs @@ -6,7 +6,7 @@ use ethrex_common::U256; use ethrex_common::types::Account; use ethrex_common::types::Code; use ethrex_common::types::CodeMetadata; -#[cfg(all(feature = "rayon", not(feature = "eip-8025")))] +#[cfg(feature = "rayon")] use ethrex_common::types::block_access_list::SlotChange; use ethrex_common::types::block_access_list::{ BalAddressIndex, BlockAccessList, BlockAccessListRecorder, @@ -43,7 +43,7 @@ pub struct LazyBalCursor { /// Returns `true` if any info field was applied; `false` if all field positions /// were 0 (no info changes for this account at indices <= max_idx). /// Does NOT touch `account.storage`. -#[cfg(all(feature = "rayon", not(feature = "eip-8025")))] +#[cfg(feature = "rayon")] pub fn seed_one_address_info_from_bal( db: &mut GeneralizedDatabase, bal: &BlockAccessList, @@ -159,7 +159,7 @@ pub fn seed_one_address_info_from_bal( /// /// Pure read; returns `Some(value)` if any `slot_changes` entry has /// `block_access_index <= max_idx`, `None` otherwise. -#[cfg(all(feature = "rayon", not(feature = "eip-8025")))] +#[cfg(feature = "rayon")] pub fn post_value_at_or_before(sc: &SlotChange, max_idx: u32) -> Option { let pos = sc .slot_changes @@ -174,7 +174,7 @@ pub fn post_value_at_or_before(sc: &SlotChange, max_idx: u32) -> Option { /// /// O(1) slot resolution via the precomputed `slot_idx_by_account` map in /// `BalAddressIndex`. Pure read; does not touch `db`. -#[cfg(all(feature = "rayon", not(feature = "eip-8025")))] +#[cfg(feature = "rayon")] pub fn seed_one_storage_slot_from_bal( bal: &BlockAccessList, index: &BalAddressIndex, @@ -190,7 +190,7 @@ pub fn seed_one_storage_slot_from_bal( } /// Compute code hash and optional `Code` object from raw bytecode in a BAL entry. -#[cfg(all(feature = "rayon", not(feature = "eip-8025")))] +#[cfg(feature = "rayon")] pub fn code_from_bal(new_code: &bytes::Bytes) -> (H256, Option) { use ethrex_common::constants::EMPTY_KECCAK_HASH; if new_code.is_empty() { @@ -396,7 +396,7 @@ impl GeneralizedDatabase { // recurse infinitely. Taking the cursor out breaks the cycle: the inner call sees // `lazy_bal = None` and falls through to `shared_base`/store. We restore the cursor // unconditionally afterward (even on error) so the outer caller still sees it. - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] { let cursor_opt = self.lazy_bal.take(); let helper_result = if let Some(cursor) = cursor_opt.as_ref() { @@ -1040,7 +1040,7 @@ impl<'a> VM<'a> { // Lazy-BAL hook: copy result out BEFORE taking &mut on current_accounts_state // so the immutable borrow of lazy_bal is released before the mutable reborrow. - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] let bal_hit: Option = self.db.lazy_bal.as_ref().and_then(|cursor| { debug_assert!( cursor.bal_index >= 1, @@ -1050,7 +1050,7 @@ impl<'a> VM<'a> { let &acct_idx = cursor.index.addr_to_idx.get(&address)?; seed_one_storage_slot_from_bal(&cursor.bal, &cursor.index, acct_idx, key, max_idx) }); - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] if let Some(value) = bal_hit { let account = self .db diff --git a/crates/vm/levm/src/db/mod.rs b/crates/vm/levm/src/db/mod.rs index be0ecc4b382..bf9056bb371 100644 --- a/crates/vm/levm/src/db/mod.rs +++ b/crates/vm/levm/src/db/mod.rs @@ -157,7 +157,7 @@ impl CachingDatabase { /// Per-slot parallel point-gets, in `missing` order. Warm-optimal fan-out /// for normal-sized prefetch batches; bloated batches use the sorted batch /// multi_get instead (see `prefetch_storage`). - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] fn point_get_storage_many( &self, missing: &[(Address, H256)], @@ -169,7 +169,7 @@ impl CachingDatabase { .collect() } - #[cfg(not(all(feature = "rayon", not(feature = "eip-8025"))))] + #[cfg(not(feature = "rayon"))] fn point_get_storage_many( &self, missing: &[(Address, H256)], @@ -214,7 +214,7 @@ impl CachingDatabase { /// Per-account parallel point-gets, in `missing` order. Warm-optimal fan-out /// for normal-sized prefetch batches; large batches use the sorted sharded /// multi_get instead (see `prefetch_accounts`). - #[cfg(all(feature = "rayon", not(feature = "eip-8025")))] + #[cfg(feature = "rayon")] fn point_get_accounts_many( &self, missing: &[Address], @@ -226,7 +226,7 @@ impl CachingDatabase { .collect() } - #[cfg(not(all(feature = "rayon", not(feature = "eip-8025"))))] + #[cfg(not(feature = "rayon"))] fn point_get_accounts_many( &self, missing: &[Address], diff --git a/test/Cargo.toml b/test/Cargo.toml index ac5c69cfc37..cdf50be85ed 100644 --- a/test/Cargo.toml +++ b/test/Cargo.toml @@ -15,7 +15,6 @@ rocksdb = ["ethrex-storage/rocksdb"] l2 = ["ethrex/l2", "ethrex/l2-sql"] c-kzg = ["ethrex-common/c-kzg"] rayon = ["ethrex-levm/rayon"] -eip-8025 = ["ethrex-levm/eip-8025"] [dependencies] ethrex-common.workspace = true diff --git a/test/tests/levm/bal_view_tests.rs b/test/tests/levm/bal_view_tests.rs index 6e6175a77aa..64b6ebe2aee 100644 --- a/test/tests/levm/bal_view_tests.rs +++ b/test/tests/levm/bal_view_tests.rs @@ -2,13 +2,13 @@ //! //! All three tests exercise the helper functions directly (unit level) because //! `seed_one_storage_slot_from_bal` and `seed_one_address_info_from_bal` are -//! `#[cfg(all(feature = "rayon", not(feature = "eip-8025")))]`-gated; reaching +//! `#[cfg(feature = "rayon")]`-gated; reaching //! `execute_block_parallel` from the test crate would require enabling that //! feature pair and wiring up a full Amsterdam chain config, block, and signed //! transactions. The helper-level tests cover the same off-by-one boundary and //! storage-injection invariants that the lazy cursor relies on. -#[cfg(all(feature = "rayon", not(feature = "eip-8025")))] +#[cfg(feature = "rayon")] mod inner { use ethereum_types::H160; use ethrex_common::{ From af510e112c70d867c43490601b0bc0973281ea09 Mon Sep 17 00:00:00 2001 From: Lucas Fiegl Date: Mon, 3 Aug 2026 14:26:08 -0300 Subject: [PATCH 02/30] fix(l1): restore rayon default for ef_tests-state --- test/tests/levm/bal_view_tests.rs | 2 +- tooling/ef_tests/state/Cargo.toml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/tests/levm/bal_view_tests.rs b/test/tests/levm/bal_view_tests.rs index 64b6ebe2aee..be241950444 100644 --- a/test/tests/levm/bal_view_tests.rs +++ b/test/tests/levm/bal_view_tests.rs @@ -1,6 +1,6 @@ //! BAL lazy-cursor regression tests. //! -//! All three tests exercise the helper functions directly (unit level) because +//! All five tests exercise the helper functions directly (unit level) because //! `seed_one_storage_slot_from_bal` and `seed_one_address_info_from_bal` are //! `#[cfg(feature = "rayon")]`-gated; reaching //! `execute_block_parallel` from the test crate would require enabling that diff --git a/tooling/ef_tests/state/Cargo.toml b/tooling/ef_tests/state/Cargo.toml index fca95ed91a0..4767451e77d 100644 --- a/tooling/ef_tests/state/Cargo.toml +++ b/tooling/ef_tests/state/Cargo.toml @@ -47,9 +47,10 @@ hex = "0.4.3" path = "./lib.rs" [features] -default = ["c-kzg", "secp256k1"] +default = ["c-kzg", "secp256k1", "rayon"] c-kzg = ["ethrex-vm/c-kzg", "ethrex-levm/c-kzg", "ethrex-common/c-kzg"] secp256k1 = ["ethrex-blockchain/secp256k1", "ethrex-common/secp256k1", "ethrex-vm/secp256k1"] +rayon = ["ethrex-blockchain/rayon", "ethrex-common/rayon", "ethrex-vm/rayon"] # Runs EF tests with no_std-compatible crypto fallbacks (k256, num-bigint, kzg-rs) nostd-crypto = ["ethrex-crypto/kzg-rs"] From 565f9c9c1f91dc33057aad0c95838e187a3e0d89 Mon Sep 17 00:00:00 2001 From: Lucas Fiegl Date: Mon, 3 Aug 2026 14:28:21 -0300 Subject: [PATCH 03/30] fix(l1): fix doc-comment lint under rayon feature --- test/tests/levm/bal_view_tests.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/tests/levm/bal_view_tests.rs b/test/tests/levm/bal_view_tests.rs index be241950444..88885ce53af 100644 --- a/test/tests/levm/bal_view_tests.rs +++ b/test/tests/levm/bal_view_tests.rs @@ -105,6 +105,7 @@ mod inner { /// at indices 1 and 2), the cursor boundary semantics are correct: /// - `max_idx = 1` returns `V0` (only tx 0's write is visible) /// - `max_idx = 2` returns `V1` (tx 1's write is also visible) + /// /// This mirrors the pre-write value tx 1 and tx 2 would observe respectively. #[test] fn sstore_sees_prior_write() { From a8c11e9db12b7a1c6fc6cf85b843cee50242d43b Mon Sep 17 00:00:00 2001 From: Lucas Fiegl Date: Mon, 3 Aug 2026 15:34:54 -0300 Subject: [PATCH 04/30] refactor(l1): drop vestigial eip-8025 features --- .github/workflows/pr_nostd.yaml | 5 ----- crates/blockchain/Cargo.toml | 1 - crates/l2/networking/rpc/Cargo.toml | 3 --- crates/networking/rpc/Cargo.toml | 1 - tooling/ef_tests/blockchain/Cargo.toml | 1 - 5 files changed, 11 deletions(-) diff --git a/.github/workflows/pr_nostd.yaml b/.github/workflows/pr_nostd.yaml index a53b5daf690..4fba42c498e 100644 --- a/.github/workflows/pr_nostd.yaml +++ b/.github/workflows/pr_nostd.yaml @@ -48,8 +48,3 @@ jobs: -p ethrex-rlp -p ethrex-crypto -p ethrex-trie --no-default-features --target "$TARGET" cargo +nightly-2026-06-29 -Z json-target-spec -Z build-std=core,alloc check \ -p ethrex-crypto --no-default-features --features kzg-rs --target "$TARGET" - # `eip-8025` currently gates no code in ethrex-trie (empty stub kept for the - # ethrex-common feature chain). This mirrors the guest's feature set and - # guards future eip-8025-gated trie code staying no_std-clean. - cargo +nightly-2026-06-29 -Z json-target-spec -Z build-std=core,alloc check \ - -p ethrex-trie --no-default-features --features eip-8025 --target "$TARGET" diff --git a/crates/blockchain/Cargo.toml b/crates/blockchain/Cargo.toml index da726d78f8f..2d8d31abba2 100644 --- a/crates/blockchain/Cargo.toml +++ b/crates/blockchain/Cargo.toml @@ -51,7 +51,6 @@ rayon = ["ethrex-vm/rayon"] secp256k1 = ["ethrex-common/secp256k1", "ethrex-vm/secp256k1"] c-kzg = ["ethrex-common/c-kzg", "ethrex-vm/c-kzg"] metrics = ["ethrex-metrics/transactions", "ethrex-metrics/metrics"] -eip-8025 = ["ethrex-common/eip-8025", "ethrex-vm/eip-8025"] testing = [] [lints.clippy] diff --git a/crates/l2/networking/rpc/Cargo.toml b/crates/l2/networking/rpc/Cargo.toml index e56b5116492..e4fbe49571a 100644 --- a/crates/l2/networking/rpc/Cargo.toml +++ b/crates/l2/networking/rpc/Cargo.toml @@ -45,9 +45,6 @@ path = "./lib.rs" # Pulls in ethrex-rpc's `test_utils` for this crate's tests only (stripped on publish). ethrex-rpc = { workspace = true, features = ["test-utils"] } -[features] -eip-8025 = ["ethrex-rpc/eip-8025"] - [lints.clippy] unwrap_used = "deny" redundant_clone = "warn" diff --git a/crates/networking/rpc/Cargo.toml b/crates/networking/rpc/Cargo.toml index 429b4680f09..ee000a8b969 100644 --- a/crates/networking/rpc/Cargo.toml +++ b/crates/networking/rpc/Cargo.toml @@ -70,7 +70,6 @@ redundant_clone = "warn" [features] jemalloc_profiling = ["dep:jemalloc_pprof"] -eip-8025 = ["ethrex-blockchain/eip-8025", "ethrex-common/eip-8025"] # Exposes the `test_utils` module (test scaffolding) to other crates' test # builds. Off by default so it is excluded from the published library. test-utils = [] diff --git a/tooling/ef_tests/blockchain/Cargo.toml b/tooling/ef_tests/blockchain/Cargo.toml index b52a8005a5c..c8e351444a9 100644 --- a/tooling/ef_tests/blockchain/Cargo.toml +++ b/tooling/ef_tests/blockchain/Cargo.toml @@ -34,7 +34,6 @@ default = ["c-kzg"] c-kzg = ["ethrex-blockchain/c-kzg"] sp1 = ["ethrex-guest-program/sp1-build-elf", "ethrex-prover/sp1"] stateless = [ - "ethrex-blockchain/eip-8025", "ethrex-common/eip-8025", "ethrex-guest-program/eip-8025", "ethrex-prover/eip-8025", From 2587d8caf68f919f987fda5bca8d0bdf943040ec Mon Sep 17 00:00:00 2001 From: Lucas Fiegl Date: Mon, 3 Aug 2026 16:18:48 -0300 Subject: [PATCH 05/30] refactor(l1): re-gate levm test mods on rayon --- crates/l2/Cargo.toml | 1 - crates/vm/Cargo.toml | 2 +- crates/vm/backends/levm/mod.rs | 10 +++++----- crates/vm/levm/Cargo.toml | 1 - 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/l2/Cargo.toml b/crates/l2/Cargo.toml index 7cb6a9f2ae8..60d99b323c7 100644 --- a/crates/l2/Cargo.toml +++ b/crates/l2/Cargo.toml @@ -92,4 +92,3 @@ metrics = ["ethrex-blockchain/metrics"] l2 = [] -eip-8025 = ["ethrex-levm/eip-8025"] diff --git a/crates/vm/Cargo.toml b/crates/vm/Cargo.toml index f5586cd4155..44230b3901a 100644 --- a/crates/vm/Cargo.toml +++ b/crates/vm/Cargo.toml @@ -37,7 +37,7 @@ default = ["secp256k1", "rayon"] rayon = ["dep:rayon", "ethrex-levm/rayon"] secp256k1 = ["ethrex-levm/secp256k1", "ethrex-common/secp256k1"] c-kzg = ["ethrex-levm/c-kzg", "ethrex-common/c-kzg"] -eip-8025 = ["ethrex-levm/eip-8025", "ethrex-common/eip-8025"] +eip-8025 = ["ethrex-common/eip-8025"] sp1 = ["ethrex-levm/sp1", "ethrex-common/sp1"] risc0 = ["ethrex-levm/risc0", "ethrex-common/risc0", "c-kzg"] diff --git a/crates/vm/backends/levm/mod.rs b/crates/vm/backends/levm/mod.rs index 1b2c0854230..139407545fd 100644 --- a/crates/vm/backends/levm/mod.rs +++ b/crates/vm/backends/levm/mod.rs @@ -4027,8 +4027,8 @@ fn describe_balance_diff(expected: U256, actual: U256) -> String { } // Exercises the rayon-parallel-BAL execution path (and shares its -// `not(eip-8025)`-gated imports), so it only builds in the non-guest test profile. -#[cfg(all(test, not(feature = "eip-8025")))] +// `rayon`-gated imports), so it only builds when that feature is on. +#[cfg(all(test, feature = "rayon"))] mod bal_tests { use super::*; use ethrex_common::H256; @@ -4388,9 +4388,9 @@ mod system_call_coinbase_tests { /// Tests for EIP-8079 burned_fees computation in execute_block (LStar-gated). /// -/// Shares the non-guest execution path's `not(eip-8025)`-gated imports -/// (`EMPTY_KECCAK_HASH`, `FxHashMap`, `Database`), so it only builds in that profile. -#[cfg(all(test, not(feature = "eip-8025")))] +/// Shares the non-guest execution path's `rayon`-gated imports +/// (`EMPTY_KECCAK_HASH`, `FxHashMap`, `Database`), so it only builds with rayon. +#[cfg(all(test, feature = "rayon"))] mod burned_fees_tests { use super::*; use ethrex_common::{ diff --git a/crates/vm/levm/Cargo.toml b/crates/vm/levm/Cargo.toml index 787104d7743..7d31f87390c 100644 --- a/crates/vm/levm/Cargo.toml +++ b/crates/vm/levm/Cargo.toml @@ -33,7 +33,6 @@ p256 = { version = "0.13.2", features = ["ecdsa", "arithmetic", "expose-field"] [features] default = [] c-kzg = ["ethrex-common/c-kzg", "ethrex-crypto/c-kzg"] -eip-8025 = ["ethrex-common/eip-8025"] ethereum_foundation_tests = [] debug = [] openvm = ["ethrex-common/openvm"] From 40aaa6e364dfe8e453b232e2dcd18e3aa3d1d0dd Mon Sep 17 00:00:00 2001 From: Lucas Fiegl Date: Tue, 4 Aug 2026 16:33:36 -0300 Subject: [PATCH 06/30] test(l1): fill #3278 conformance vectors --- .gitignore | 2 + tooling/ef_tests/blockchain/Makefile | 9 +- .../scripts/gen_stateless_vectors.sh | 83 +++++++++++++++++++ 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100755 tooling/ef_tests/blockchain/scripts/gen_stateless_vectors.sh diff --git a/.gitignore b/.gitignore index a706162a4eb..5764f85e57b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,8 @@ tooling/ef_tests/blockchain/vectors tooling/ef_tests/blockchain/vectors_zkevm +tooling/ef_tests/blockchain/vectors_stateless_3278 +tooling/ef_tests/blockchain/.stateless-vector-work tooling/ef_tests/state/vectors diff --git a/tooling/ef_tests/blockchain/Makefile b/tooling/ef_tests/blockchain/Makefile index 95f8faa16b1..f0201bb7704 100644 --- a/tooling/ef_tests/blockchain/Makefile +++ b/tooling/ef_tests/blockchain/Makefile @@ -1,4 +1,4 @@ -.PHONY: download-test-vectors clean-vectors test test-levm test-sp1 test-stateless amsterdam-vectors zkevm-vectors test-stateless-zkevm +.PHONY: download-test-vectors clean-vectors test test-levm test-sp1 test-stateless amsterdam-vectors zkevm-vectors test-stateless-zkevm stateless-vector VECTORS_ROOT := vectors FIXTURES_FILE := ../.fixtures_url @@ -65,6 +65,13 @@ $(ZKEVM_VECTORS_DIR): $(ZKEVM_ARTIFACT) mkdir -p $(ZKEVM_VECTORS_DIR) tar -xzf $(ZKEVM_ARTIFACT) --strip-components=2 -C $(ZKEVM_VECTORS_DIR) fixtures/blockchain_tests/for_amsterdam +# Conformance vectors for execution-specs 3c3b6f4af (#3248 progressive SSZ + +# #3278 ChainConfig removal). No tests-zkevm release carries that schema, and +# v0.6.2 reuses schema id 0x1501 for the OLD body, so its vectors misparse. +# Generated, never committed. See scripts/gen_stateless_vectors.sh. +stateless-vector: + scripts/gen_stateless_vectors.sh + zkevm-vectors: $(ZKEVM_VECTORS_DIR) help: ## 📚 Show help for each of the Makefile recipes diff --git a/tooling/ef_tests/blockchain/scripts/gen_stateless_vectors.sh b/tooling/ef_tests/blockchain/scripts/gen_stateless_vectors.sh new file mode 100755 index 00000000000..b4e0935aaff --- /dev/null +++ b/tooling/ef_tests/blockchain/scripts/gen_stateless_vectors.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# +# Generate stateless-validation conformance vectors by filling the upstream +# EIP-8025 tests at a pinned execution-specs commit. +# +# Pinned to 3c3b6f4af315b268a61e20d5a4da8aa4f24c91f0 -- execution-specs master at +# the merge of #3278, which is #3248 (progressive SSZ) + #3278 (ChainConfig +# removal). No tests-zkevm release carries this schema: v0.6.2 (2026-07-13) +# predates both, and it reuses schema id 0x1501 for the OLD body, so its vectors +# misparse against this schema. Delete this script and switch back to +# zkevm-vectors when a tests-zkevm v0.7.x exists. +# +# Two upstream quirks are load-bearing: +# - Python 3.14 cannot build coincurve (scikit-build-core); we pin 3.12 via uv. +# - The spec's wheel omits forks/amsterdam/execution_engine/, so we run the +# source tree via PYTHONPATH and install only its dependencies. Do not +# "fix" this by relying on the installed package. +# +# Usage: gen_stateless_vectors.sh [output-dir] + +set -euo pipefail + +SPEC_SHA=3c3b6f4af315b268a61e20d5a4da8aa4f24c91f0 +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +OUT="${1:-$ROOT/vectors_stateless_3278}" +WORK="${STATELESS_VECTOR_WORKDIR:-$ROOT/.stateless-vector-work}" +SPECS="$WORK/execution-specs" +VENV="$WORK/venv" + +command -v uv >/dev/null || { echo "uv is required (brew install uv)" >&2; exit 1; } + +mkdir -p "$WORK" +if [[ ! -d $SPECS/.git ]]; then + git clone --filter=blob:none --quiet https://github.com/ethereum/execution-specs "$SPECS" +fi +git -C "$SPECS" checkout --quiet "$SPEC_SHA" +[[ "$(git -C "$SPECS" rev-parse HEAD)" == "$SPEC_SHA" ]] || { + echo "checkout drifted from $SPEC_SHA" >&2; exit 1; } + +if [[ ! -x $VENV/bin/fill ]]; then + uv venv --python 3.12 "$VENV" + VIRTUAL_ENV="$VENV" uv pip install --quiet "$SPECS" + VIRTUAL_ENV="$VENV" uv pip install --quiet "$SPECS/packages/testing" +fi + +rm -rf "$OUT" +# PYTHONPATH shadows the installed copy with the source tree; see the header. +PYTHONPATH="$SPECS/src" "$VENV/bin/fill" \ + "$SPECS/tests/amsterdam/eip8025_optional_proofs/" \ + --fork Amsterdam --output "$OUT" -q --no-html + +# Fail loudly if the vector set is not what the downstream tasks assume. +"$VENV/bin/python" - "$OUT" <<'EOF' +import json, os, sys + +out = sys.argv[1] +succ = fail = 0 +bad_len = [] +# os.walk, not glob("**"): `fill` embeds the input path in the output path, so the +# fixtures sit under the dot-prefixed work dir, and glob's ** skips hidden dirs. +files = [ + os.path.join(d, n) + for d, _, ns in os.walk(os.path.join(out, "blockchain_tests")) + for n in ns + if n.endswith(".json") +] +for f in files: + for _, t in json.load(open(f)).items(): + for b in t.get("blocks", []): + o = b.get("statelessOutputBytes") + if not o: + continue + raw = bytes.fromhex(o.removeprefix("0x")) + if len(raw) != 43: + bad_len.append(len(raw)) + succ += raw[32] == 1 + fail += raw[32] == 0 +print(f"vectors: {succ} success, {fail} failure") +if bad_len: + sys.exit(f"FAIL: non-43-byte outputs {sorted(set(bad_len))} -- pre-#3278 schema?") +if succ == 0: + sys.exit("FAIL: no true-success vector; the set cannot prove execution works") +EOF From ddead9e656645d236f42639608ad9cb846f9fe7c Mon Sep 17 00:00:00 2001 From: Lucas Fiegl Date: Tue, 4 Aug 2026 16:59:28 -0300 Subject: [PATCH 07/30] test(l1): pin progressive SSZ vs remerkleable --- Cargo.lock | 2 + test/Cargo.toml | 2 + test/tests/common/mod.rs | 1 + test/tests/common/progressive_ssz_tests.rs | 138 +++++++++++++++++++++ 4 files changed, 143 insertions(+) create mode 100644 test/tests/common/progressive_ssz_tests.rs diff --git a/Cargo.lock b/Cargo.lock index 0822dd86c49..86fc23ce4a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4482,6 +4482,8 @@ dependencies = [ "k256", "lazy_static", "libssz", + "libssz-merkle", + "libssz-types", "once_cell", "p256", "proptest", diff --git a/test/Cargo.toml b/test/Cargo.toml index cdf50be85ed..36219fbf17a 100644 --- a/test/Cargo.toml +++ b/test/Cargo.toml @@ -63,6 +63,8 @@ reqwest.workspace = true tokio-util.workspace = true ethrex-guest-program = { workspace = true, default-features = false } libssz.workspace = true +libssz-merkle.workspace = true +libssz-types.workspace = true ethrex-crypto.workspace = true ethrex-metrics = { workspace = true, features = ["metrics"] } serial_test = "3" diff --git a/test/tests/common/mod.rs b/test/tests/common/mod.rs index edda100bdf9..61a67479943 100644 --- a/test/tests/common/mod.rs +++ b/test/tests/common/mod.rs @@ -8,6 +8,7 @@ mod eip7702_authorization_tests; mod frame_tx_validation_tests; mod legacy_signature_tests; mod logs_bloom_validation_tests; +mod progressive_ssz_tests; mod requests_eip8282_tests; mod rkyv_utils_tests; mod serde_utils_tests; diff --git a/test/tests/common/progressive_ssz_tests.rs b/test/tests/common/progressive_ssz_tests.rs new file mode 100644 index 00000000000..af8355bf8ff --- /dev/null +++ b/test/tests/common/progressive_ssz_tests.rs @@ -0,0 +1,138 @@ +//! Cross-implementation parity for EIP-7916 progressive merkleization. +//! +//! execution-specs at `3c3b6f4af` (#3248, "change StatelessInput SSZ +//! serialization to be EIP-7688 aligned") moves the stateless payload +//! containers to `ProgressiveList` and `ProgressiveContainer`. ethrex computes +//! those roots with `libssz-merkle`, the spec with `remerkleable` — two +//! independent implementations of the same EIP. Nothing in the type system +//! forces them to agree, and a divergence would silently change every +//! `new_payload_request_root` the guest commits to. +//! +//! The expected roots below were produced by remerkleable at that pinned +//! commit. Sizes 0, 1, 2, 5 and 21 straddle the progressive subtree boundaries +//! (leaf counts grow 1, 4, 16, 64, …), so they exercise the recursion rather +//! than just the base case. +//! +//! Regenerate with, from an execution-specs checkout at the pin: +//! +//! ```text +//! PYTHONPATH=src python -c " +//! from remerkleable.progressive import ProgressiveList +//! from remerkleable.basic import uint64 +//! for n in (0,1,2,5,21): +//! v = ProgressiveList[uint64](*[uint64(i+1) for i in range(n)]) +//! print(n, v.hash_tree_root().hex())" +//! ``` + +// `Crypto` is imported for its `sha256` method, which the bridge below calls. +use ethrex_crypto::{Crypto, NativeCrypto}; +use libssz_merkle::{HashTreeRoot, Sha256Hasher}; +use libssz_types::ProgressiveList; + +/// Bridges `ethrex-crypto` to the SSZ hasher, mirroring what the guest does so +/// this test exercises the same hashing path production code uses. +struct CryptoHasher(NativeCrypto); + +impl Sha256Hasher for CryptoHasher { + fn hash(&self, data: &[u8]) -> [u8; 32] { + self.0.sha256(data) + } +} + +/// remerkleable `ProgressiveList[uint64]` roots at execution-specs `3c3b6f4af`. +const REMERKLEABLE_UINT64_ROOTS: &[(usize, &str)] = &[ + ( + 0, + "f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b", + ), + ( + 1, + "905efb51c2764c2c7a4efb0548e372569df06db82115c3b1896c186632f3fe5b", + ), + ( + 2, + "4250789d7838bee417a2b0d7639d928b05e8b75f1fc59588a4301b6e8f70ba58", + ), + ( + 5, + "29918e0447260511bc5be0f7dbb9817201e16e30c56af228b9cb931a16e8799d", + ), + ( + 21, + "ed360c03ecbdfbb6f4b1cf5d9cbf6887038423e31121700797de968a9969aaed", + ), +]; + +/// KNOWN FAILING — `libssz-merkle 0.2.2` has the progressive subtree children +/// swapped relative to the spec, so every progressive root it computes is wrong. +/// +/// `merkleize_progressive_inner` (libssz-merkle-0.2.2/src/lib.rs:151) ends with +/// `hash_nodes(hasher, &rest, &subtree)` — remainder left, subtree right — and a +/// comment claiming parity with ethereum/remerkleable `667eab00`. Current +/// remerkleable does the opposite (`remerkleable/progressive.py:29`): +/// +/// ```text +/// PairNode( +/// subtree_fill_to_contents(nodes[:base_size], depth), # LEFT = subtree +/// subtree_fill_progressive(nodes[base_size:], depth+2), # RIGHT = remainder +/// ) +/// ``` +/// +/// Proven by hand for a single `uint64(1)`, where the packed chunk is +/// `01 00.. || 24 zero bytes` and `Z` is a zero node: +/// +/// ```text +/// mix_in_length(hash(Z || chunk), 1) = 573a032d… <- libssz's output +/// mix_in_length(hash(chunk || Z), 1) = 905efb51… <- the spec's root +/// ``` +/// +/// The leaf-count progression agrees (libssz's `num_leaves *= 4` matches +/// remerkleable's `depth + 2`); only the child order differs. The fix is to swap +/// that one `hash_nodes` argument pair in `libssz-merkle`. +/// +/// This blocks adopting execution-specs #3248: ethrex cannot compute a correct +/// `new_payload_request_root` until it is fixed, because `SszExecutionPayload` +/// and `SszExecutionRequests` are progressive containers and every payload's +/// root flows through this function. Remove `#[ignore]` once `libssz` is fixed — +/// the assertion is already correct. +#[test] +#[ignore = "libssz-merkle 0.2.2 swaps progressive subtree children; see doc comment"] +fn progressive_list_roots_match_remerkleable() { + let hasher = CryptoHasher(NativeCrypto); + + for &(n, expected_hex) in REMERKLEABLE_UINT64_ROOTS { + let list: ProgressiveList = (1..=n as u64).collect::>().into(); + let got = list.hash_tree_root(&hasher); + let expected: [u8; 32] = hex::decode(expected_hex) + .expect("static hex is valid") + .try_into() + .expect("32 bytes"); + + assert_eq!( + got, + expected, + "progressive-list root diverged from remerkleable at n={n}:\n \ + libssz {}\n remerkleable {expected_hex}", + hex::encode(got), + ); + } +} + +/// The empty list is the one case a broken implementation is most likely to get +/// right by accident (zero hash), so assert the non-empty cases move the root. +#[test] +fn progressive_list_roots_are_distinct_per_length() { + let hasher = CryptoHasher(NativeCrypto); + let mut seen = Vec::new(); + + for n in 0..=21u64 { + let list: ProgressiveList = (1..=n).collect::>().into(); + let root = list.hash_tree_root(&hasher); + assert!( + !seen.contains(&root), + "length {n} collided with an earlier length; \ + mix_in_length is not being applied" + ); + seen.push(root); + } +} From 270504db4efbd5654e96a422d9741ea2a6d9c0c6 Mon Sep 17 00:00:00 2001 From: Lucas Fiegl Date: Tue, 4 Aug 2026 17:05:03 -0300 Subject: [PATCH 08/30] test(l1): cover progressive container root parity --- test/tests/common/progressive_ssz_tests.rs | 60 ++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/test/tests/common/progressive_ssz_tests.rs b/test/tests/common/progressive_ssz_tests.rs index af8355bf8ff..a2f6a2e9035 100644 --- a/test/tests/common/progressive_ssz_tests.rs +++ b/test/tests/common/progressive_ssz_tests.rs @@ -118,6 +118,66 @@ fn progressive_list_roots_match_remerkleable() { } } +/// The shape #3248 actually uses: `SszExecutionPayload` is +/// `ProgressiveContainer(active_fields=[1; 19])` and `SszExecutionRequests` is +/// `[1; 5]`. There is no libssz type for this, so production code composes +/// `mix_in_active_fields(merkleize_progressive(field_roots), &[true; N])` by hand +/// — and so does this test, which is why it is the one that matters most. +/// +/// Ignored for the same reason as above: `merkleize_progressive` is wrong. The +/// surrounding primitives are not — `mix_in_active_fields`, `merkleize`, `pack` +/// and `mix_in_length` were all verified against remerkleable independently, so +/// swapping the two children in `libssz` makes every case here pass. +/// +/// Reference roots from remerkleable at the pin: +/// +/// ```text +/// PC(active_fields=[1,1,1], x=1, y=2, z=3) -> e9ff4f89… +/// PC(active_fields=[1;5], a=1, b=2, c=3, d=4, e=5) -> 5a167eaf… +/// ``` +#[test] +#[ignore = "libssz-merkle 0.2.2 swaps progressive subtree children; see doc comment"] +fn progressive_container_roots_match_remerkleable() { + use libssz_merkle::{Node, merkleize_progressive, mix_in_active_fields}; + + let hasher = CryptoHasher(NativeCrypto); + + for (n, expected_hex) in [ + ( + 3usize, + "e9ff4f8918d6640489dbb084574dbaf57cc7e8a5b4cd1fcd904a7af79a0dc89d", + ), + ( + 5, + "5a167eafdb77037933df6b87009c5d116ef0b6e6800d37b2e70693875b64318d", + ), + ] { + // Each field is a uint64 i, whose root is the value LE in a zero-padded chunk. + let field_roots: Vec = (1..=n as u64) + .map(|i| { + let mut chunk = [0u8; 32]; + chunk[..8].copy_from_slice(&i.to_le_bytes()); + chunk + }) + .collect(); + + let root = merkleize_progressive(&hasher, &field_roots); + let got = mix_in_active_fields(&hasher, &root, &vec![true; n]); + let expected: [u8; 32] = hex::decode(expected_hex) + .expect("static hex is valid") + .try_into() + .expect("32 bytes"); + + assert_eq!( + got, + expected, + "progressive-container root diverged at {n} fields:\n \ + libssz {}\n remerkleable {expected_hex}", + hex::encode(got), + ); + } +} + /// The empty list is the one case a broken implementation is most likely to get /// right by accident (zero hash), so assert the non-empty cases move the root. #[test] From 4ece074efe1445afb60f3d7e0910642bc055ab58 Mon Sep 17 00:00:00 2001 From: Lucas Fiegl Date: Tue, 4 Aug 2026 17:18:04 -0300 Subject: [PATCH 09/30] feat(l1): adopt progressive SSZ + EIP-8282 requests --- crates/common/types/stateless_ssz.rs | 203 +++++++++++++----- .../l2/sequencer/native_rollup/l1_advancer.rs | 53 +++-- crates/vm/levm/src/execute_precompile.rs | 16 +- test/tests/l2/native_rollup_sol_offsets.rs | 16 +- 4 files changed, 190 insertions(+), 98 deletions(-) diff --git a/crates/common/types/stateless_ssz.rs b/crates/common/types/stateless_ssz.rs index d55d6147f60..8e393fc52d5 100644 --- a/crates/common/types/stateless_ssz.rs +++ b/crates/common/types/stateless_ssz.rs @@ -10,8 +10,10 @@ use bytes::Bytes; use libssz::{SszDecode, SszEncode}; use libssz_derive::{HashTreeRoot, SszDecode, SszEncode}; -use libssz_merkle::{HashTreeRoot, Sha256Hasher}; -use libssz_types::{SszList, SszVector}; +use libssz_merkle::{ + HashTreeRoot, Node, Sha256Hasher, merkleize_progressive, mix_in_active_fields, +}; +use libssz_types::{ProgressiveList, SszList, SszVector}; use super::requests::EncodedRequests; @@ -21,28 +23,16 @@ use super::requests::EncodedRequests; // ── Spec limits (Electra) ────────────────────────────────────────── -/// `MAX_TRANSACTIONS_PER_PAYLOAD` (Electra). -const MAX_TRANSACTIONS_PER_PAYLOAD: usize = 1_048_576; -/// `MAX_WITHDRAWALS_PER_PAYLOAD` (Electra). -const MAX_WITHDRAWALS_PER_PAYLOAD: usize = 16; -/// `MAX_BYTES_PER_TRANSACTION`. -const MAX_BYTES_PER_TRANSACTION: usize = 1_073_741_824; /// `MAX_EXTRA_DATA_BYTES`. const MAX_EXTRA_DATA_BYTES: usize = 32; -/// `MAX_DEPOSIT_REQUESTS_PER_PAYLOAD` (Electra). -const MAX_DEPOSIT_REQUESTS_PER_PAYLOAD: usize = 8192; -/// `MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD` (Electra). -const MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: usize = 16; -/// `MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD` (Electra). -const MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: usize = 2; -/// `MAX_BLOB_COMMITMENTS_PER_BLOCK` (Electra). -const MAX_BLOB_COMMITMENTS_PER_BLOCK: usize = 4096; // ── EIP-7685 request type prefixes ───────────────────────────────── const DEPOSIT_REQUEST_TYPE: u8 = 0x00; const WITHDRAWAL_REQUEST_TYPE: u8 = 0x01; const CONSOLIDATION_REQUEST_TYPE: u8 = 0x02; +const BUILDER_DEPOSIT_REQUEST_TYPE: u8 = 0x03; +const BUILDER_EXIT_REQUEST_TYPE: u8 = 0x04; // ── Bytes20 wrapper (address) ────────────────────────────────────── // @@ -150,10 +140,32 @@ pub struct ConsolidationRequest { pub target_pubkey: [u8; 48], } -// ── ExecutionPayload ─────────────────────────────────────────────── +/// SSZ `BuilderDepositRequest` container (EIP-8282). +#[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode, HashTreeRoot)] +pub struct BuilderDepositRequest { + pub pubkey: [u8; 48], + pub withdrawal_credentials: [u8; 32], + pub amount: u64, + pub signature: [u8; 96], +} -/// SSZ container matching the `85fc20ca` `SszExecutionPayload` (Electra fields + EIP-7928 `block_access_list`; 18 fields). +/// SSZ `BuilderExitRequest` container (EIP-8282). #[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode, HashTreeRoot)] +pub struct BuilderExitRequest { + pub source_address: Bytes20, + pub pubkey: [u8; 48], +} + +// ── ExecutionPayload ─────────────────────────────────────────────── + +/// SSZ container matching `SszExecutionPayload` at execution-specs `3c3b6f4af` +/// (#3248): a `ProgressiveContainer(active_fields=[1; 19])` whose transaction, +/// withdrawal and block-access-list fields are progressive lists. +/// +/// `HashTreeRoot` is hand-written because `libssz-derive` has no progressive +/// support; encode/decode still derive, since nothing progressive changes the +/// wire layout (`ProgressiveList` delegates both to `Vec`). +#[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode)] pub struct ExecutionPayload { pub parent_hash: [u8; 32], pub fee_recipient: Bytes20, @@ -169,34 +181,88 @@ pub struct ExecutionPayload { /// `base_fee_per_gas` encoded as a 256-bit unsigned integer (little-endian). pub base_fee_per_gas: [u8; 32], pub block_hash: [u8; 32], - pub transactions: SszList, MAX_TRANSACTIONS_PER_PAYLOAD>, - pub withdrawals: SszList, + pub transactions: ProgressiveList>, + pub withdrawals: ProgressiveList, pub blob_gas_used: u64, pub excess_blob_gas: u64, /// EIP-7928 block-level access list (full serialized BAL bytes). - pub block_access_list: SszList, + pub block_access_list: ProgressiveList, /// EIP-7843 slot number (Amsterdam+). Last field, matching the execution-specs /// `ExecutionPayloadV4` layout so the native path is byte-compatible with the /// spec-current payload. Provided by the L2 producer and carried to L1. pub slot_number: u64, } +/// `ProgressiveContainer` merkleization, per EIP-7495/EIP-7916: +/// `mix_in_active_fields(merkleize_progressive(field_roots), active_fields)`. +/// +/// Hand-written because `libssz-derive 0.2.2` has no progressive support. +/// +/// NOTE: `libssz-merkle 0.2.2`'s `merkleize_progressive` has its subtree children +/// reversed relative to the reference implementation, so the roots produced here +/// are wrong until that is fixed upstream. The composition below is correct — see +/// `test/tests/common/progressive_ssz_tests.rs`, whose two `#[ignore]`d cases +/// pass the moment `libssz` swaps that argument pair. Every other primitive used +/// here was verified against remerkleable. +fn progressive_container_root( + hasher: &impl Sha256Hasher, + field_roots: [Node; N], +) -> Node { + let root = merkleize_progressive(hasher, &field_roots); + mix_in_active_fields(hasher, &root, &[true; N]) +} + +impl HashTreeRoot for ExecutionPayload { + fn hash_tree_root(&self, hasher: &impl Sha256Hasher) -> Node { + // Declaration order is the SSZ field order; do not reorder. + progressive_container_root( + hasher, + [ + self.parent_hash.hash_tree_root(hasher), + self.fee_recipient.hash_tree_root(hasher), + self.state_root.hash_tree_root(hasher), + self.receipts_root.hash_tree_root(hasher), + self.logs_bloom.hash_tree_root(hasher), + self.prev_randao.hash_tree_root(hasher), + self.block_number.hash_tree_root(hasher), + self.gas_limit.hash_tree_root(hasher), + self.gas_used.hash_tree_root(hasher), + self.timestamp.hash_tree_root(hasher), + self.extra_data.hash_tree_root(hasher), + self.base_fee_per_gas.hash_tree_root(hasher), + self.block_hash.hash_tree_root(hasher), + self.transactions.hash_tree_root(hasher), + self.withdrawals.hash_tree_root(hasher), + self.blob_gas_used.hash_tree_root(hasher), + self.excess_blob_gas.hash_tree_root(hasher), + self.block_access_list.hash_tree_root(hasher), + self.slot_number.hash_tree_root(hasher), + ], + ) + } +} + // ── ExecutionRequests ────────────────────────────────────────────── -/// SSZ `ExecutionRequests` container (Electra) — the typed EIP-7685 bundle -/// that the CL commits to alongside `ExecutionPayload`. -#[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode, HashTreeRoot)] +/// SSZ `ExecutionRequests` at execution-specs `3c3b6f4af`: a +/// `ProgressiveContainer(active_fields=[1; 5])` carrying the EIP-7685 bundle +/// including the EIP-8282 builder requests. +/// +/// `HashTreeRoot` is hand-written; see [`ExecutionPayload`]. +#[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode)] pub struct ExecutionRequests { - pub deposits: SszList, - pub withdrawals: SszList, - pub consolidations: SszList, + pub deposits: ProgressiveList, + pub withdrawals: ProgressiveList, + pub consolidations: ProgressiveList, + pub builder_deposits: ProgressiveList, + pub builder_exits: ProgressiveList, } impl ExecutionRequests { - /// Produce the EIP-7685 encoded form: three `EncodedRequests` entries, + /// Produce the EIP-7685 encoded form: five `EncodedRequests` entries, /// one per request type, each `[type_byte] ++ concat(ssz_encode(item))`. /// - /// The three request types are all fixed-size SSZ containers, so their + /// The five request types are all fixed-size SSZ containers, so their /// SSZ encoding is byte-for-byte the EL wire concatenation that /// `compute_requests_hash` expects. pub fn to_encoded_requests(&self) -> Vec { @@ -219,10 +285,33 @@ impl ExecutionRequests { CONSOLIDATION_REQUEST_TYPE, self.consolidations.iter().cloned(), ), + encode( + BUILDER_DEPOSIT_REQUEST_TYPE, + self.builder_deposits.iter().cloned(), + ), + encode( + BUILDER_EXIT_REQUEST_TYPE, + self.builder_exits.iter().cloned(), + ), ] } } +impl HashTreeRoot for ExecutionRequests { + fn hash_tree_root(&self, hasher: &impl Sha256Hasher) -> Node { + progressive_container_root( + hasher, + [ + self.deposits.hash_tree_root(hasher), + self.withdrawals.hash_tree_root(hasher), + self.consolidations.hash_tree_root(hasher), + self.builder_deposits.hash_tree_root(hasher), + self.builder_exits.hash_tree_root(hasher), + ], + ) + } +} + // ── NewPayloadRequest ────────────────────────────────────────────── /// SSZ `NewPayloadRequest` — the key container whose `hash_tree_root` is @@ -230,7 +319,7 @@ impl ExecutionRequests { #[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode, HashTreeRoot)] pub struct NewPayloadRequest { pub execution_payload: ExecutionPayload, - pub versioned_hashes: SszList<[u8; 32], MAX_BLOB_COMMITMENTS_PER_BLOCK>, + pub versioned_hashes: ProgressiveList<[u8; 32]>, pub parent_beacon_block_root: [u8; 32], pub execution_requests: ExecutionRequests, } @@ -276,8 +365,6 @@ const MAX_BYTES_PER_HEADER: usize = 1_024; // 2^10 const MAX_PUBLIC_KEYS: usize = 1_048_576; // 2^20 /// PUBLIC_KEY_BYTES — an uncompressed secp256k1 public key is 65 bytes. const PUBLIC_KEY_BYTES: usize = 65; -/// MAX_BLOCK_ACCESS_LIST_BYTES — EIP-7928 BAL byte cap. -const MAX_BLOCK_ACCESS_LIST_BYTES: usize = 16_777_216; // 2^24 /// MAX_BLOB_SCHEDULES_PER_FORK — SSZ Optional[BlobSchedule] as List[T, 1]. const MAX_BLOB_SCHEDULES_PER_FORK: usize = 1; /// MAX_FORK_ACTIVATION_VALUES — SSZ Optional[uint64] as List[uint64, 1]. @@ -430,23 +517,25 @@ mod tests { .expect("withdrawals fit"), blob_gas_used: 0, excess_blob_gas: 0, - block_access_list: SszList::new(), // TODO(Plan 02): populate full BAL + block_access_list: ProgressiveList::new(), // TODO(Plan 02): populate full BAL slot_number: 0, } } fn empty_requests() -> ExecutionRequests { ExecutionRequests { - deposits: vec![].try_into().expect("empty deposits"), - withdrawals: vec![].try_into().expect("empty withdrawals"), - consolidations: vec![].try_into().expect("empty consolidations"), + deposits: Vec::new().into(), + withdrawals: Vec::new().into(), + consolidations: Vec::new().into(), + builder_deposits: Vec::new().into(), + builder_exits: Vec::new().into(), } } fn sample_request() -> NewPayloadRequest { NewPayloadRequest { execution_payload: sample_payload(), - versioned_hashes: vec![].try_into().expect("empty versioned_hashes"), + versioned_hashes: Vec::new().into(), parent_beacon_block_root: [8u8; 32], execution_requests: empty_requests(), } @@ -483,26 +572,28 @@ mod tests { signature: [0x33; 96], index: 7, }] - .try_into() - .expect("one deposit fits"), + .into(), withdrawals: vec![WithdrawalRequest { source_address: Bytes20([0x44; 20]), validator_pubkey: [0x55; 48], amount: 1_000_000, }] - .try_into() - .expect("one withdrawal fits"), + .into(), consolidations: vec![ConsolidationRequest { source_address: Bytes20([0x66; 20]), source_pubkey: [0x77; 48], target_pubkey: [0x88; 48], }] - .try_into() - .expect("one consolidation fits"), + .into(), + builder_deposits: ProgressiveList::new(), + builder_exits: ProgressiveList::new(), }; let encoded = requests.to_encoded_requests(); - assert_eq!(encoded.len(), 3, "must emit 3 EIP-7685 entries"); + // Five entries since EIP-8282: the two builder lists are empty here, and + // `compute_requests_hash` skips entries of length <= 1, so an empty + // builder list leaves the requests hash unchanged. + assert_eq!(encoded.len(), 5, "must emit 5 EIP-7685 entries"); // Deposit: [0x00] ++ 192 bytes assert_eq!(encoded[0].0[0], DEPOSIT_REQUEST_TYPE); @@ -618,11 +709,11 @@ mod tests { extra_data: list(vec![0xde, 0xad]), base_fee_per_gas: [0u8; 32], block_hash: [0x66; 32], - transactions: SszList::new(), - withdrawals: SszList::new(), + transactions: ProgressiveList::new(), + withdrawals: ProgressiveList::new(), blob_gas_used: 0, excess_blob_gas: 0, - block_access_list: list(vec![0x01, 0x02, 0x03]), + block_access_list: vec![0x01u8, 0x02, 0x03].into(), slot_number: 0, }; round_trip(&payload); @@ -704,11 +795,11 @@ mod tests { extra_data: SszList::new(), base_fee_per_gas: [0u8; 32], block_hash: [0x66; 32], - transactions: SszList::new(), - withdrawals: SszList::new(), + transactions: ProgressiveList::new(), + withdrawals: ProgressiveList::new(), blob_gas_used: 0, excess_blob_gas: 0, - block_access_list: SszList::new(), + block_access_list: ProgressiveList::new(), slot_number: 0x7843, } } @@ -754,12 +845,14 @@ mod tests { let ep = sample_execution_payload(); let npr = NewPayloadRequest { execution_payload: ep, - versioned_hashes: SszList::new(), + versioned_hashes: ProgressiveList::new(), parent_beacon_block_root: [0x00; 32], execution_requests: ExecutionRequests { - deposits: SszList::new(), - withdrawals: SszList::new(), - consolidations: SszList::new(), + deposits: ProgressiveList::new(), + withdrawals: ProgressiveList::new(), + consolidations: ProgressiveList::new(), + builder_deposits: ProgressiveList::new(), + builder_exits: ProgressiveList::new(), }, }; let input = SszStatelessInput { @@ -1013,7 +1106,7 @@ mod tests { .expect("withdrawals fit"), blob_gas_used: ep.blob_gas_used, excess_blob_gas: ep.excess_blob_gas, - block_access_list: SszList::new(), + block_access_list: ProgressiveList::new(), slot_number: ep.slot_number, }; assert_eq!( diff --git a/crates/l2/sequencer/native_rollup/l1_advancer.rs b/crates/l2/sequencer/native_rollup/l1_advancer.rs index 8d4c5421fd8..45cea3bed57 100644 --- a/crates/l2/sequencer/native_rollup/l1_advancer.rs +++ b/crates/l2/sequencer/native_rollup/l1_advancer.rs @@ -251,21 +251,17 @@ impl NativeL1Advancer { /// preimage of `block_access_list_hash` (see `BlockAccessList::compute_hash`), /// so the guest-program reconstruction can decode it and recompute the same /// hash. `None` (pre-Amsterdam) → empty list. +/// Build the SSZ `block_access_list` field. +/// +/// Since execution-specs #3248 this is a `ProgressiveList`, which carries no +/// length bound, so the conversion is infallible — the `Result` is gone. pub fn bal_to_ssz_block_access_list( bal: Option<ðrex_common::types::block_access_list::BlockAccessList>, -) -> Result< - libssz_types::SszList< - u8, - // Mirrors ExecutionPayload.block_access_list field in stateless_ssz.rs (2^24). - 16_777_216, - >, - String, -> { +) -> libssz_types::ProgressiveList { use ethrex_rlp::encode::RLPEncode; match bal { - Some(bal) => libssz_types::SszList::try_from(bal.encode_to_vec()) - .map_err(|e| format!("block_access_list exceeds MAX_BLOCK_ACCESS_LIST_BYTES: {e:?}")), - None => Ok(libssz_types::SszList::new()), + Some(bal) => bal.encode_to_vec().into(), + None => libssz_types::ProgressiveList::new(), } } @@ -281,22 +277,19 @@ pub fn build_ssz_stateless_input( ) -> Result, String> { use ethrex_common::types::stateless_ssz::*; use libssz::SszEncode; - use libssz_types::SszList; + use libssz_types::{ProgressiveList, SszList}; // 1. Convert Block → SSZ ExecutionPayload - let transactions: Vec> = body + // Both levels are progressive lists since #3248, so neither can overflow a + // bound and the conversions are infallible. + let transactions: Vec> = body .transactions .iter() - .enumerate() - .map(|(i, tx)| { - SszList::try_from(tx.encode_canonical_to_vec()) - .map_err(|e| format!("transaction[{i}] exceeds MAX_BYTES_PER_TRANSACTION: {e:?}")) - }) - .collect::>()?; - let ssz_transactions = SszList::try_from(transactions) - .map_err(|e| format!("transactions exceed MAX_TRANSACTIONS_PER_PAYLOAD: {e:?}"))?; + .map(|tx| tx.encode_canonical_to_vec().into()) + .collect(); + let ssz_transactions: ProgressiveList> = transactions.into(); - let ssz_withdrawals = SszList::new(); // Empty for L2 + let ssz_withdrawals = ProgressiveList::new(); // Empty for L2 // base_fee_per_gas as LE uint256 let mut base_fee_bytes = [0u8; 32]; @@ -335,7 +328,7 @@ pub fn build_ssz_stateless_input( withdrawals: ssz_withdrawals, blob_gas_used: header.blob_gas_used.unwrap_or(0), excess_blob_gas: header.excess_blob_gas.unwrap_or(0), - block_access_list: bal_to_ssz_block_access_list(bal)?, + block_access_list: bal_to_ssz_block_access_list(bal), // EIP-7843: carry the header's slot number into the SSZ payload so L1 can // read it and the reconstructed block hash matches. slot_number: header.slot_number.unwrap_or(0), @@ -349,14 +342,16 @@ pub fn build_ssz_stateless_input( // L2 blocks never carry EIP-7685 requests. let execution_requests = ExecutionRequests { - deposits: SszList::new(), - withdrawals: SszList::new(), - consolidations: SszList::new(), + deposits: ProgressiveList::new(), + withdrawals: ProgressiveList::new(), + consolidations: ProgressiveList::new(), + builder_deposits: ProgressiveList::new(), + builder_exits: ProgressiveList::new(), }; let new_payload_request = NewPayloadRequest { execution_payload, - versioned_hashes: SszList::new(), // Empty for L2 + versioned_hashes: ProgressiveList::new(), // Empty for L2 parent_beacon_block_root, execution_requests, }; @@ -531,7 +526,7 @@ mod devnet_tests { use ethrex_rlp::encode::RLPEncode; // None → empty SSZ list (pre-Amsterdam). - let empty = bal_to_ssz_block_access_list(None).expect("none encodes"); + let empty = bal_to_ssz_block_access_list(None); assert_eq!(empty.len(), 0); // Some(bal) → the SSZ bytes equal the BAL's RLP encoding, so that @@ -539,7 +534,7 @@ mod devnet_tests { let bal = BlockAccessList::from_accounts(vec![AccountChanges::new(Address::from([0x11u8; 20]))]); let expected = bal.encode_to_vec(); - let got = bal_to_ssz_block_access_list(Some(&bal)).expect("some encodes"); + let got = bal_to_ssz_block_access_list(Some(&bal)); let got_bytes: Vec = got.iter().copied().collect(); assert_eq!(got_bytes, expected); assert!(!got_bytes.is_empty()); diff --git a/crates/vm/levm/src/execute_precompile.rs b/crates/vm/levm/src/execute_precompile.rs index f72068927fc..c69e2266447 100644 --- a/crates/vm/levm/src/execute_precompile.rs +++ b/crates/vm/levm/src/execute_precompile.rs @@ -201,19 +201,21 @@ mod tests { extra_data: vec![].try_into().expect("extra_data"), base_fee_per_gas: [0u8; 32], block_hash: [0u8; 32], - transactions: vec![].try_into().expect("transactions"), - withdrawals: vec![].try_into().expect("withdrawals"), + transactions: Vec::new().into(), + withdrawals: Vec::new().into(), blob_gas_used: 0, excess_blob_gas: 0, - block_access_list: vec![].try_into().expect("block_access_list"), + block_access_list: Vec::new().into(), slot_number: 0, }, - versioned_hashes: vec![].try_into().expect("versioned_hashes"), + versioned_hashes: Vec::new().into(), parent_beacon_block_root: [0u8; 32], execution_requests: ExecutionRequests { - deposits: vec![].try_into().expect("deposits"), - withdrawals: vec![].try_into().expect("withdrawals"), - consolidations: vec![].try_into().expect("consolidations"), + deposits: Vec::new().into(), + withdrawals: Vec::new().into(), + consolidations: Vec::new().into(), + builder_deposits: Vec::new().into(), + builder_exits: Vec::new().into(), }, }, witness: SszExecutionWitness { diff --git a/test/tests/l2/native_rollup_sol_offsets.rs b/test/tests/l2/native_rollup_sol_offsets.rs index 158e61a3ead..d3081b8e1a0 100644 --- a/test/tests/l2/native_rollup_sol_offsets.rs +++ b/test/tests/l2/native_rollup_sol_offsets.rs @@ -80,11 +80,11 @@ fn sample_execution_payload() -> ExecutionPayload { extra_data: vec![].try_into().expect("extra_data"), base_fee_per_gas: [0u8; 32], block_hash: [0x66; 32], - transactions: vec![].try_into().expect("transactions"), - withdrawals: vec![].try_into().expect("withdrawals"), + transactions: Vec::new().into(), + withdrawals: Vec::new().into(), blob_gas_used: 0, excess_blob_gas: 0, - block_access_list: vec![].try_into().expect("block_access_list"), + block_access_list: Vec::new().into(), slot_number: 0x7843, } } @@ -104,12 +104,14 @@ fn encode_sample_input() -> Vec { let input = SszStatelessInput { new_payload_request: NewPayloadRequest { execution_payload: sample_execution_payload(), - versioned_hashes: vec![].try_into().expect("versioned_hashes"), + versioned_hashes: Vec::new().into(), parent_beacon_block_root: [0u8; 32], execution_requests: ExecutionRequests { - deposits: vec![].try_into().expect("deposits"), - withdrawals: vec![].try_into().expect("withdrawals"), - consolidations: vec![].try_into().expect("consolidations"), + deposits: Vec::new().into(), + withdrawals: Vec::new().into(), + consolidations: Vec::new().into(), + builder_deposits: Vec::new().into(), + builder_exits: Vec::new().into(), }, }, witness: SszExecutionWitness { From 3f286da75b4b8dfc8830bff5cd6b78c36b83dd60 Mon Sep 17 00:00:00 2001 From: Lucas Fiegl Date: Tue, 4 Aug 2026 17:47:51 -0300 Subject: [PATCH 10/30] feat(l1): drop wire ChainConfig, add schema_id --- crates/blockchain/stateless.rs | 11 +- .../common/types/block_execution_witness.rs | 196 +++++------------- crates/common/types/stateless_ssz.rs | 179 ++++++---------- .../src/nativeRollup/l1/NativeRollup.sol | 39 +++- .../l2/sequencer/native_rollup/l1_advancer.rs | 8 +- crates/vm/levm/src/execute_precompile.rs | 41 +--- test/tests/l2/native_rollup_sol_offsets.rs | 52 ++--- 7 files changed, 183 insertions(+), 343 deletions(-) diff --git a/crates/blockchain/stateless.rs b/crates/blockchain/stateless.rs index d9ef544f20d..f8777bc619b 100644 --- a/crates/blockchain/stateless.rs +++ b/crates/blockchain/stateless.rs @@ -11,7 +11,7 @@ use std::sync::Arc; use ethrex_common::types::block_execution_witness::ExecutionWitness; use ethrex_common::types::stateless_ssz::{ - NewPayloadRequest, SszChainConfig, SszStatelessInput, SszStatelessValidationResult, + NewPayloadRequest, SszStatelessInput, SszStatelessValidationResult, }; use ethrex_crypto::Crypto; use ethrex_guest_program::common::ExecutionError; @@ -29,7 +29,7 @@ use libssz_merkle::{HashTreeRoot, Sha2Hasher}; pub fn verify_stateless_new_payload( new_payload_request: &NewPayloadRequest, execution_witness: ExecutionWitness, - chain_config: &SszChainConfig, + chain_id: u64, crypto: Arc, ) -> SszStatelessValidationResult { let request_root = new_payload_request.hash_tree_root(&Sha2Hasher); @@ -42,10 +42,13 @@ pub fn verify_stateless_new_payload( } }; + // `chain_id` and `schema_id` are echoed even when validation fails; only a + // decode failure produces the all-zero default, which happens before this. SszStatelessValidationResult { new_payload_request_root: request_root, successful_validation: successful, - chain_config: chain_config.clone(), + chain_id, + schema_id: ethrex_common::types::stateless_ssz::STATELESS_INPUT_SCHEMA_ID, } } @@ -91,7 +94,7 @@ impl ethrex_vm::StatelessValidator for StatelessExecutor { let result = verify_stateless_new_payload( &input.new_payload_request, execution_witness, - &input.chain_config, + input.chain_id, self.crypto.clone(), ); diff --git a/crates/common/types/block_execution_witness.rs b/crates/common/types/block_execution_witness.rs index 6714c0bdd36..ac330592b6a 100644 --- a/crates/common/types/block_execution_witness.rs +++ b/crates/common/types/block_execution_witness.rs @@ -287,9 +287,9 @@ impl ExecutionWitness { /// (`number == first_block_number - 1`) among the witness headers — same /// convention as `RpcExecutionWitness::into_execution_witness`. /// - /// The `ChainConfig` is derived from the SSZ `active_fork` via - /// `ssz_chain_config_to_internal` — every fork up to the active one is - /// activated at timestamp 0 (native-L2 genesis activation). + /// The `ChainConfig` is derived from the input's `chain_id` alone via + /// [`amsterdam_chain_config`]; #3278 removed chain configuration from the + /// wire, so the fork comes from the schema-id prefix instead. pub fn from_ssz( input: &crate::types::stateless_ssz::SszStatelessInput, ) -> Result { @@ -318,7 +318,7 @@ impl ExecutionWitness { codes: input.witness.codes_as_vecs(), block_headers_bytes: input.witness.headers_as_vecs(), first_block_number, - chain_config: ssz_chain_config_to_internal(&input.chain_config)?, + chain_config: amsterdam_chain_config(input.chain_id), state_trie_root, storage_trie_roots, }) @@ -1018,87 +1018,27 @@ fn set_hash_or_validate(header: &BlockHeader, hash: H256) -> Result<(), GuestPro Ok(()) } -/// Map an ethrex `Fork` to its spec `PROTOCOL_FORKS` index (execution-specs 85fc20ca). -fn fork_to_spec_index(fork: crate::types::genesis::Fork) -> Result { - use crate::types::genesis::Fork; - Ok(match fork { - Fork::Cancun => 16, - Fork::Prague => 17, - Fork::Osaka => 18, - Fork::Amsterdam => 24, - other => { - return Err(GuestProgramStateError::Custom(format!( - "fork {other:?} has no stateless spec fork index (native rollups run Cancun+)" - ))); - } - }) -} - -/// Encode an ethrex `ChainConfig` into the SSZ `SszChainConfig` (with `active_fork`). -/// The active fork is resolved at `block_timestamp`; the native L2 activates its -/// forks at genesis, so `activation.timestamp = [0]`. -pub fn chain_config_to_ssz( - cfg: &ChainConfig, - block_timestamp: u64, -) -> Result { - use crate::types::stateless_ssz::{ - SszBlobSchedule, SszChainConfig, SszForkActivation, SszForkConfig, SszOptionalBlobSchedule, - SszOptionalForkActivationValue, - }; - - let fork = cfg.get_fork(block_timestamp); - let fork_index = fork_to_spec_index(fork)?; - - let mut timestamp: SszOptionalForkActivationValue = SszOptionalForkActivationValue::new(); - timestamp - .push(0u64) - .map_err(|e| GuestProgramStateError::Custom(format!("activation ts push: {e:?}")))?; - - let mut blob_schedule: SszOptionalBlobSchedule = SszOptionalBlobSchedule::new(); - if let Some(bs) = cfg.get_fork_blob_schedule(block_timestamp) { - blob_schedule - .push(SszBlobSchedule { - target: bs.target as u64, - max: bs.max as u64, - base_fee_update_fraction: bs.base_fee_update_fraction, - }) - .map_err(|e| GuestProgramStateError::Custom(format!("blob_schedule push: {e:?}")))?; - } - - Ok(SszChainConfig { - chain_id: cfg.chain_id, - active_fork: SszForkConfig { - fork: fork_index, - activation: SszForkActivation { - block_number: SszOptionalForkActivationValue::new(), - timestamp, - }, - blob_schedule, - }, - }) -} - -/// Decode an `SszChainConfig` into an ethrex `ChainConfig`. Activates every fork -/// up to and including the SSZ `active_fork` at timestamp 0 (native-L2 genesis -/// activation). `blob_schedule` is left `Default` — L2 blocks carry no blobs, so -/// it does not affect execution. -pub fn ssz_chain_config_to_internal( - scc: &crate::types::stateless_ssz::SszChainConfig, -) -> Result { - use crate::types::genesis::Fork; - let fork = match scc.active_fork.fork { - 16 => Fork::Cancun, - 17 => Fork::Prague, - 18 => Fork::Osaka, - 24 => Fork::Amsterdam, - other => { - return Err(GuestProgramStateError::Custom(format!( - "unknown/unsupported spec fork index {other}" - ))); - } - }; - Ok(ChainConfig { - chain_id: scc.chain_id, +/// Build the `ChainConfig` for a stateless input, from its `chain_id` alone. +/// +/// execution-specs #3278 removed `ChainConfig` from the wire: activation info and +/// blob schedules are guest-internal knowledge, keyed by `(chain_id, fork)`, with +/// the fork fixed by the schema-id prefix — always Amsterdam (`0x1501`) at that +/// pin, since `deserialize_stateless_input` rejects every other id. +/// +/// Every fork up to and including Amsterdam is activated at 0 and no +/// payload-timestamp-versus-activation check is performed. That mirrors EEST, +/// which ships one implementation per fork and therefore skips the check — and +/// matching it is what keeps ethrex byte-identical to the conformance vectors +/// generated from the reference. +/// +/// TODO(upstream): `verify_stateless_new_payload` in `stateless.py` comments that +/// "a real implementation MUST do these checks", but #3278 leaves no activation +/// data on the wire to check against. Revisit if upstream reintroduces one, or +/// specifies where a real client should source it. See +/// https://github.com/ethereum/execution-specs/pull/3278 +pub fn amsterdam_chain_config(chain_id: u64) -> ChainConfig { + ChainConfig { + chain_id, homestead_block: Some(0), eip150_block: Some(0), eip155_block: Some(0), @@ -1111,76 +1051,46 @@ pub fn ssz_chain_config_to_internal( london_block: Some(0), terminal_total_difficulty: Some(0), terminal_total_difficulty_passed: true, - shanghai_time: (fork >= Fork::Shanghai).then_some(0), - cancun_time: (fork >= Fork::Cancun).then_some(0), - prague_time: (fork >= Fork::Prague).then_some(0), - osaka_time: (fork >= Fork::Osaka).then_some(0), - amsterdam_time: (fork >= Fork::Amsterdam).then_some(0), + shanghai_time: Some(0), + cancun_time: Some(0), + prague_time: Some(0), + osaka_time: Some(0), + amsterdam_time: Some(0), ..Default::default() - }) + } } #[cfg(test)] -mod active_fork_tests { +mod amsterdam_chain_config_tests { use super::*; - use crate::types::genesis::{ChainConfig, Fork}; - - fn prague_l2_config() -> ChainConfig { - ChainConfig { - chain_id: 1, - homestead_block: Some(0), - eip150_block: Some(0), - eip155_block: Some(0), - eip158_block: Some(0), - byzantium_block: Some(0), - constantinople_block: Some(0), - petersburg_block: Some(0), - istanbul_block: Some(0), - berlin_block: Some(0), - london_block: Some(0), - terminal_total_difficulty: Some(0), - terminal_total_difficulty_passed: true, - shanghai_time: Some(0), - cancun_time: Some(0), - prague_time: Some(0), - ..Default::default() - } - } #[test] - fn active_fork_round_trips_prague() { - let cfg = prague_l2_config(); - let ssz = chain_config_to_ssz(&cfg, 0).expect("encode"); - // Encodes the spec Prague index (17) at genesis activation. - assert_eq!(ssz.active_fork.fork, 17); - assert_eq!(ssz.chain_id, 1); - let back = ssz_chain_config_to_internal(&ssz).expect("decode"); - // Fork rules reproduce: Prague active, Osaka/Amsterdam inactive. - assert_eq!(back.get_fork(0), Fork::Prague); - assert_eq!(back.chain_id, 1); - assert!(back.osaka_time.is_none()); - assert!(back.amsterdam_time.is_none()); + fn activates_every_fork_through_amsterdam_at_zero() { + let cfg = amsterdam_chain_config(1); + assert_eq!(cfg.chain_id, 1); + for (name, v) in [ + ("shanghai", cfg.shanghai_time), + ("cancun", cfg.cancun_time), + ("prague", cfg.prague_time), + ("osaka", cfg.osaka_time), + ("amsterdam", cfg.amsterdam_time), + ] { + assert_eq!(v, Some(0), "{name} must be active from 0"); + } + assert!(cfg.terminal_total_difficulty_passed); } + /// Forks past Amsterdam must stay unscheduled: the schema id pins Amsterdam, + /// so activating a later fork would apply rules the input never asked for. #[test] - fn active_fork_round_trips_cancun() { - let mut cfg = prague_l2_config(); - cfg.prague_time = None; // Cancun-only L2 - let ssz = chain_config_to_ssz(&cfg, 0).expect("encode"); - assert_eq!(ssz.active_fork.fork, 16); - let back = ssz_chain_config_to_internal(&ssz).expect("decode"); - assert_eq!(back.get_fork(0), Fork::Cancun); - assert!(back.prague_time.is_none()); + fn leaves_post_amsterdam_forks_unscheduled() { + let cfg = amsterdam_chain_config(1); + assert_eq!(cfg.hegota_time, None); + assert_eq!(cfg.lstar_time, None); } #[test] - fn active_fork_round_trips_amsterdam() { - let mut cfg = prague_l2_config(); - cfg.osaka_time = Some(0); - cfg.amsterdam_time = Some(0); - let ssz = chain_config_to_ssz(&cfg, 0).expect("encode"); - assert_eq!(ssz.active_fork.fork, 24); // spec Amsterdam index - let back = ssz_chain_config_to_internal(&ssz).expect("decode"); - assert_eq!(back.get_fork(0), Fork::Amsterdam); + fn carries_the_chain_id_through() { + assert_eq!(amsterdam_chain_config(u64::MAX).chain_id, u64::MAX); } } diff --git a/crates/common/types/stateless_ssz.rs b/crates/common/types/stateless_ssz.rs index 8e393fc52d5..e0b99788923 100644 --- a/crates/common/types/stateless_ssz.rs +++ b/crates/common/types/stateless_ssz.rs @@ -5,7 +5,7 @@ //! tree-hashing `NewPayloadRequest` and producing the `PublicInput` //! committed to by execution proofs. The second section layers the //! native-rollup types (`SszStatelessInput`, `SszStatelessValidationResult`, -//! `SszExecutionWitness`, `SszChainConfig`) on top of those. +//! `SszExecutionWitness`) on top of those. use bytes::Bytes; use libssz::{SszDecode, SszEncode}; @@ -365,10 +365,6 @@ const MAX_BYTES_PER_HEADER: usize = 1_024; // 2^10 const MAX_PUBLIC_KEYS: usize = 1_048_576; // 2^20 /// PUBLIC_KEY_BYTES — an uncompressed secp256k1 public key is 65 bytes. const PUBLIC_KEY_BYTES: usize = 65; -/// MAX_BLOB_SCHEDULES_PER_FORK — SSZ Optional[BlobSchedule] as List[T, 1]. -const MAX_BLOB_SCHEDULES_PER_FORK: usize = 1; -/// MAX_FORK_ACTIVATION_VALUES — SSZ Optional[uint64] as List[uint64, 1]. -const MAX_FORK_ACTIVATION_VALUES: usize = 1; // ── Stateless validation types ─────────────────────────────────── // @@ -376,40 +372,21 @@ const MAX_FORK_ACTIVATION_VALUES: usize = 1; // commit EIP-8025 PR #11604 pins: // https://github.com/ethereum/execution-specs/blob/85fc20ca5937719a854472a87cb48d01ef1dffca/src/ethereum/forks/amsterdam/stateless_ssz.py -/// SSZ Optional[uint64] modelled as `List[uint64, 1]`. -pub type SszOptionalForkActivationValue = SszList; -/// SSZ Optional[BlobSchedule] modelled as `List[SszBlobSchedule, 1]`. -pub type SszOptionalBlobSchedule = SszList; - -/// SSZ `BlobSchedule` — effective blob params for a fork. -#[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode, HashTreeRoot)] -pub struct SszBlobSchedule { - pub target: u64, - pub max: u64, - pub base_fee_update_fraction: u64, -} - -/// SSZ `ForkActivation` — the (optional) block/timestamp a fork activates at. -#[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode, HashTreeRoot)] -pub struct SszForkActivation { - pub block_number: SszOptionalForkActivationValue, - pub timestamp: SszOptionalForkActivationValue, -} +/// Schema id of the stateless input wire format: `fork_index << 8 | revision`, +/// where fork `0x15` is Amsterdam and revision `0x01` is the current encoding. +/// +/// The 2-byte big-endian prefix is how the fork reaches the guest at all — #3278 +/// removed all chain configuration from the SSZ body — and it is echoed as a +/// public output field so a verifier can pin which rules were applied. Upstream +/// rejects any other id outright; so does ethrex. +/// +/// Note that upstream reused `0x1501` across an incompatible body change +/// (#3248 + #3278 vs `tests-zkevm@v0.6.2`), so the id alone does not distinguish +/// the two dialects. ethrex speaks the newer one. +pub const STATELESS_INPUT_SCHEMA_ID: u16 = 0x1501; -/// SSZ `ForkConfig` — the active fork id, its activation, and blob schedule. -#[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode, HashTreeRoot)] -pub struct SszForkConfig { - pub fork: u64, - pub activation: SszForkActivation, - pub blob_schedule: SszOptionalBlobSchedule, -} - -/// SSZ `ChainConfig` container. Variable-size (nests `active_fork`'s lists). -#[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode, HashTreeRoot)] -pub struct SszChainConfig { - pub chain_id: u64, - pub active_fork: SszForkConfig, -} +/// Byte length of the big-endian [`STATELESS_INPUT_SCHEMA_ID`] prefix. +pub const STATELESS_INPUT_SCHEMA_ID_SIZE: usize = 2; /// SSZ `ExecutionWitness` container matching the execution-specs definition. /// @@ -432,7 +409,10 @@ pub struct SszExecutionWitness { pub struct SszStatelessInput { pub new_payload_request: NewPayloadRequest, pub witness: SszExecutionWitness, - pub chain_config: SszChainConfig, + /// Chain identifier. #3278 removed `ChainConfig` from the wire: fork + /// activation info and blob schedules are guest-internal, keyed by + /// `(chain_id, fork)`, with the fork coming from the schema-id prefix. + pub chain_id: u64, pub public_keys: SszList, MAX_PUBLIC_KEYS>, } @@ -441,7 +421,13 @@ pub struct SszStatelessInput { pub struct SszStatelessValidationResult { pub new_payload_request_root: [u8; 32], pub successful_validation: bool, - pub chain_config: SszChainConfig, + /// Chain identifier echoed from the input — real even when validation fails; + /// only a decode failure yields the all-zero default. + pub chain_id: u64, + /// The full schema id the guest decoded (`0x1501` for Amsterdam revision 1). + /// Public so a verifier can pin which fork rules and encoding were used, for + /// forks that share a payload shape. Added by execution-specs #3278. + pub schema_id: u16, } // ── Conversions to internal types ──────────────────────────────── @@ -610,38 +596,6 @@ mod tests { // ── Stateless helpers ──────────────────────────────────────── - fn sample_active_fork() -> SszForkConfig { - SszForkConfig { - fork: 25, // Amsterdam (spec ProtocolFork index) - activation: SszForkActivation { - block_number: SszList::new(), - timestamp: { - let mut v = SszList::new(); - v.push(0u64).expect("one value fits"); - v - }, - }, - blob_schedule: SszList::new(), - } - } - - #[test] - fn ssz_chain_config_with_active_fork_round_trip() { - round_trip(&SszChainConfig { - chain_id: 1, - active_fork: sample_active_fork(), - }); - round_trip(&SszForkActivation { - block_number: SszList::new(), - timestamp: SszList::new(), - }); - round_trip(&SszBlobSchedule { - target: 3, - max: 6, - base_fee_update_fraction: 3_338_477, - }); - } - fn list(items: Vec) -> SszList { let mut list = SszList::new(); for item in items { @@ -657,22 +611,6 @@ mod tests { assert_eq!(*value, decoded, "round-trip mismatch"); } - #[test] - fn ssz_chain_config_round_trip() { - round_trip(&SszChainConfig { - chain_id: 1, - active_fork: sample_active_fork(), - }); - round_trip(&SszChainConfig { - chain_id: 0, - active_fork: sample_active_fork(), - }); - round_trip(&SszChainConfig { - chain_id: u64::MAX, - active_fork: sample_active_fork(), - }); - } - #[test] fn ssz_execution_witness_round_trip() { let witness = SszExecutionWitness { @@ -735,20 +673,16 @@ mod tests { let result = SszStatelessValidationResult { new_payload_request_root: [0xab; 32], successful_validation: true, - chain_config: SszChainConfig { - chain_id: 42, - active_fork: sample_active_fork(), - }, + chain_id: 42, + schema_id: 0x1501, }; round_trip(&result); let result_false = SszStatelessValidationResult { new_payload_request_root: [0x00; 32], successful_validation: false, - chain_config: SszChainConfig { - chain_id: 1, - active_fork: sample_active_fork(), - }, + chain_id: 1, + schema_id: 0x1501, }; round_trip(&result_false); } @@ -761,8 +695,12 @@ mod tests { // reading the wrong bytes on L1. const SOL_RESULT_SUCCESS_OFFSET: usize = 32; - // result bytes 33..36 hold the u32 LE OFFSET to chain_config's variable data (not chain_id itself). - const SOL_RESULT_CHAIN_CONFIG_OFFSET_POS: usize = 33; + // Since #3278 the result is entirely fixed-size (32 + 1 + 8 + 2 = 43 bytes): + // chain_id is read directly at 33, schema_id at 41. There is no longer an + // offset to dereference. + const SOL_RESULT_CHAIN_ID_OFFSET: usize = 33; + const SOL_RESULT_SCHEMA_ID_OFFSET: usize = 41; + const SOL_RESULT_FIXED_LEN: usize = 43; const SOL_EP_STATE_ROOT_OFFSET: usize = 52; const SOL_EP_BLOCK_NUMBER_OFFSET: usize = 404; const SOL_EP_GAS_LIMIT_OFFSET: usize = 412; @@ -807,35 +745,42 @@ mod tests { #[test] fn nativerollup_sol_result_layout_matches() { // Encode a StatelessValidationResult and confirm the contract's fixed - // offsets: successful_validation @32, chain_config offset @33, and chain_id - // (first field of chain_config) at the dereferenced offset. + // offsets. Under #3278 every field is fixed-size, so all three are direct + // reads and the total length is exactly 43 bytes. let result = SszStatelessValidationResult { new_payload_request_root: [0xAA; 32], successful_validation: true, - chain_config: SszChainConfig { - chain_id: 0x1122334455667788, - active_fork: sample_active_fork(), - }, + chain_id: 0x1122334455667788, + schema_id: 0x1501, }; let mut buf = Vec::new(); result.ssz_append(&mut buf); + assert_eq!( + buf.len(), + SOL_RESULT_FIXED_LEN, + "result must be exactly 43 fixed bytes; a variable tail means \ + ChainConfig is still in there" + ); assert_eq!( buf[SOL_RESULT_SUCCESS_OFFSET], 1, "successful_validation must be byte 32" ); - let cc_off = u32_le(&buf, SOL_RESULT_CHAIN_CONFIG_OFFSET_POS); - assert_eq!( - cc_off, 37, - "chain_config offset value must be 37 (fixed part length), actual: {}", - cc_off + let chain_id = u64::from_le_bytes( + buf[SOL_RESULT_CHAIN_ID_OFFSET..SOL_RESULT_CHAIN_ID_OFFSET + 8] + .try_into() + .unwrap(), ); - // chain_id is the first field of SszChainConfig, uint64 LE, at cc_off. - let chain_id = u64::from_le_bytes(buf[cc_off..cc_off + 8].try_into().unwrap()); assert_eq!( chain_id, 0x1122334455667788, - "chain_id must be readable at the deref offset" + "chain_id must be a direct u64 LE read at 33" ); + let schema_id = u16::from_le_bytes( + buf[SOL_RESULT_SCHEMA_ID_OFFSET..SOL_RESULT_SCHEMA_ID_OFFSET + 2] + .try_into() + .unwrap(), + ); + assert_eq!(schema_id, 0x1501, "schema_id must be a u16 LE read at 41"); } #[test] @@ -862,16 +807,16 @@ mod tests { codes: SszList::new(), headers: SszList::new(), }, - chain_config: SszChainConfig { - chain_id: 1, - active_fork: sample_active_fork(), - }, + chain_id: 1, public_keys: SszList::new(), }; let mut buf = Vec::new(); input.ssz_append(&mut buf); - // StatelessInput fixed part = 4 offsets (16 bytes); new_payload_request is field 0. + // StatelessInput fixed part = npr offset(4) + witness offset(4) + + // chain_id(8) + public_keys offset(4) = 20 bytes. new_payload_request is + // still field 0, so its offset is at byte 0 and the contract's dynamic + // read there is unaffected. let npr_abs = u32_le(&buf, 0); // NewPayloadRequest fixed prefix: execution_payload offset @ npr_abs. let ep_abs = npr_abs + u32_le(&buf, npr_abs); diff --git a/crates/l2/contracts/src/nativeRollup/l1/NativeRollup.sol b/crates/l2/contracts/src/nativeRollup/l1/NativeRollup.sol index e9106b8b75d..feb79ccc3db 100644 --- a/crates/l2/contracts/src/nativeRollup/l1/NativeRollup.sol +++ b/crates/l2/contracts/src/nativeRollup/l1/NativeRollup.sol @@ -96,12 +96,19 @@ contract NativeRollup { // test/tests/l2/native_rollup_sol_offsets.rs. uint256 constant MAX_L2_GAS_LIMIT = 16_777_216; - // SSZ `StatelessValidationResult`: root(32) + successful_validation(1, @32) + - // chain_config(variable → 4-byte offset @33). chain_config's first field is - // chain_id (uint64 LE), read at the dereferenced offset. - uint256 constant RESULT_FIXED_LEN = 37; // root(32) + bool(1) + chain_config offset(4) + // SSZ `StatelessValidationResult` is entirely fixed-size since execution-specs + // #3278 removed `ChainConfig` from the wire: + // root(32) + successful_validation(1, @32) + chain_id(8 LE, @33) + schema_id(2 LE, @41) + // = 43 bytes, with no offset to dereference. Pinned by + // test/tests/l2/native_rollup_sol_offsets.rs. + uint256 constant RESULT_FIXED_LEN = 43; uint256 constant RESULT_SUCCESS_OFFSET = 32; - uint256 constant RESULT_CHAIN_CONFIG_OFFSET_POS = 33; + uint256 constant RESULT_CHAIN_ID_OFFSET = 33; + uint256 constant RESULT_SCHEMA_ID_OFFSET = 41; + + // The only stateless-input schema this rollup accepts: fork 0x15 (Amsterdam), + // revision 0x01. The guest echoes it so we can pin which rules were applied. + uint16 constant EXPECTED_SCHEMA_ID = 0x1501; // Byte offsets inside the SSZ `ExecutionPayload` fixed prefix. uint256 constant EP_PARENT_HASH_OFFSET = 0; @@ -283,14 +290,20 @@ contract NativeRollup { (bool success, bytes memory result) = EXECUTE_PRECOMPILE.staticcall(sszInput); require(success, "NativeRollup: EXECUTE precompile failed"); - require(result.length >= RESULT_FIXED_LEN, "NativeRollup: invalid result length"); + // Exact length, not a minimum: the container is fully fixed-size, so any + // other length means the guest is speaking a different schema. + require(result.length == RESULT_FIXED_LEN, "NativeRollup: invalid result length"); require(uint8(result[RESULT_SUCCESS_OFFSET]) == 1, "NativeRollup: L2 validation failed"); - // chain_config is variable-size (contains active_fork); it is offset-encoded. - // chain_id is its first field. - uint256 ccOffset = _decodeSszUint32LE(result, RESULT_CHAIN_CONFIG_OFFSET_POS); - uint64 provenChainId = _decodeSszUint64LE(result, ccOffset); + // Both remaining fields are direct reads now that chain_config is gone. + uint64 provenChainId = _decodeSszUint64LE(result, RESULT_CHAIN_ID_OFFSET); require(provenChainId == chainId, "NativeRollup: chain_id mismatch"); + + // Pin the schema the guest decoded. Upstream reused id 0x1501 across an + // incompatible body change, so this is a guard against a guest built + // against the older dialect, not just against a future fork. + uint16 provenSchemaId = _decodeSszUint16LE(result, RESULT_SCHEMA_ID_OFFSET); + require(provenSchemaId == EXPECTED_SCHEMA_ID, "NativeRollup: schema_id mismatch"); } function _checkAndDecodeProvenFields( @@ -497,6 +510,12 @@ contract NativeRollup { return value; } + /// @dev Decode an SSZ `uint16` (2 little-endian bytes) at `offset` into `data` (memory). + function _decodeSszUint16LE(bytes memory data, uint256 offset) internal pure returns (uint16) { + require(data.length >= offset + 2, "SSZ: u16 out of bounds"); + return uint16(uint8(data[offset])) | (uint16(uint8(data[offset + 1])) << 8); + } + /// @dev Decode an SSZ `uint32` (4 little-endian bytes) at `offset` into `data` (calldata). function _readU32LECalldata(bytes calldata data, uint256 offset) internal pure returns (uint256) { require(data.length >= offset + 4, "SSZ: u32 out of bounds"); diff --git a/crates/l2/sequencer/native_rollup/l1_advancer.rs b/crates/l2/sequencer/native_rollup/l1_advancer.rs index 45cea3bed57..d225978add1 100644 --- a/crates/l2/sequencer/native_rollup/l1_advancer.rs +++ b/crates/l2/sequencer/native_rollup/l1_advancer.rs @@ -363,11 +363,9 @@ pub fn build_ssz_stateless_input( let stateless_input = SszStatelessInput { new_payload_request, witness: ssz_witness, - chain_config: ethrex_common::types::block_execution_witness::chain_config_to_ssz( - &witness.chain_config, - header.timestamp, - ) - .map_err(|e| format!("active_fork encode: {e:?}"))?, + // #3278: only the chain id crosses the wire. The consumer derives the rest + // from (chain_id, fork), with the fork coming from the schema-id prefix. + chain_id: witness.chain_config.chain_id, public_keys: SszList::new(), // Empty for now }; diff --git a/crates/vm/levm/src/execute_precompile.rs b/crates/vm/levm/src/execute_precompile.rs index c69e2266447..8fd1647b181 100644 --- a/crates/vm/levm/src/execute_precompile.rs +++ b/crates/vm/levm/src/execute_precompile.rs @@ -146,8 +146,7 @@ mod tests { use bytes::Bytes; use ethrex_common::types::stateless_ssz::{ Bytes20, DepositRequest, ExecutionPayload, ExecutionRequests, NewPayloadRequest, - SszChainConfig, SszExecutionWitness, SszForkActivation, SszForkConfig, SszStatelessInput, - SszStatelessValidationResult, Withdrawal, + SszExecutionWitness, SszStatelessInput, SszStatelessValidationResult, Withdrawal, }; use libssz::SszEncode; @@ -163,17 +162,8 @@ mod tests { let result = SszStatelessValidationResult { new_payload_request_root: [0u8; 32], successful_validation: true, - chain_config: SszChainConfig { - chain_id: 1, - active_fork: SszForkConfig { - fork: 0, - activation: SszForkActivation { - block_number: vec![].try_into().expect("empty block_number"), - timestamp: vec![].try_into().expect("empty timestamp"), - }, - blob_schedule: vec![].try_into().expect("empty blob_schedule"), - }, - }, + chain_id: 1, + schema_id: 0x1501, }; let mut buf = Vec::new(); result.ssz_append(&mut buf); @@ -223,17 +213,7 @@ mod tests { codes: vec![].try_into().expect("codes"), headers: vec![].try_into().expect("headers"), }, - chain_config: SszChainConfig { - chain_id: 1, - active_fork: SszForkConfig { - fork: 0, - activation: SszForkActivation { - block_number: vec![].try_into().expect("block_number"), - timestamp: vec![].try_into().expect("timestamp"), - }, - blob_schedule: vec![].try_into().expect("blob_schedule"), - }, - }, + chain_id: 1, public_keys: vec![].try_into().expect("public_keys"), } } @@ -243,17 +223,8 @@ mod tests { let result = SszStatelessValidationResult { new_payload_request_root: [0u8; 32], successful_validation: false, - chain_config: SszChainConfig { - chain_id: 1, - active_fork: SszForkConfig { - fork: 0, - activation: SszForkActivation { - block_number: vec![].try_into().expect("empty block_number"), - timestamp: vec![].try_into().expect("empty timestamp"), - }, - blob_schedule: vec![].try_into().expect("empty blob_schedule"), - }, - }, + chain_id: 1, + schema_id: 0x1501, }; let mut buf = Vec::new(); result.ssz_append(&mut buf); diff --git a/test/tests/l2/native_rollup_sol_offsets.rs b/test/tests/l2/native_rollup_sol_offsets.rs index d3081b8e1a0..424c74f1921 100644 --- a/test/tests/l2/native_rollup_sol_offsets.rs +++ b/test/tests/l2/native_rollup_sol_offsets.rs @@ -17,9 +17,8 @@ #![allow(clippy::unwrap_used)] use ethrex_common::types::stateless_ssz::{ - Bytes20, ExecutionPayload, ExecutionRequests, NewPayloadRequest, SszChainConfig, - SszExecutionWitness, SszForkActivation, SszForkConfig, SszStatelessInput, - SszStatelessValidationResult, + Bytes20, ExecutionPayload, ExecutionRequests, NewPayloadRequest, SszExecutionWitness, + SszStatelessInput, SszStatelessValidationResult, }; use libssz::SszEncode; @@ -89,17 +88,6 @@ fn sample_execution_payload() -> ExecutionPayload { } } -fn empty_fork_config() -> SszForkConfig { - SszForkConfig { - fork: 0, - activation: SszForkActivation { - block_number: vec![].try_into().expect("block_number"), - timestamp: vec![].try_into().expect("timestamp"), - }, - blob_schedule: vec![].try_into().expect("blob_schedule"), - } -} - fn encode_sample_input() -> Vec { let input = SszStatelessInput { new_payload_request: NewPayloadRequest { @@ -119,10 +107,7 @@ fn encode_sample_input() -> Vec { codes: vec![].try_into().expect("codes"), headers: vec![].try_into().expect("headers"), }, - chain_config: SszChainConfig { - chain_id: 1, - active_fork: empty_fork_config(), - }, + chain_id: 1, public_keys: vec![].try_into().expect("public_keys"), }; let mut buf = Vec::new(); @@ -215,10 +200,8 @@ fn sol_result_offsets_match_encoding() { let result = SszStatelessValidationResult { new_payload_request_root: [0xAA; 32], successful_validation: true, - chain_config: SszChainConfig { - chain_id: 0x1122334455667788, - active_fork: empty_fork_config(), - }, + chain_id: 0x1122334455667788, + schema_id: ethrex_common::types::stateless_ssz::STATELESS_INPUT_SCHEMA_ID, }; let mut buf = Vec::new(); result.ssz_append(&mut buf); @@ -229,18 +212,29 @@ fn sol_result_offsets_match_encoding() { "RESULT_SUCCESS_OFFSET does not point at successful_validation" ); - let cc_off_pos = sol_uint_const(&src, "RESULT_CHAIN_CONFIG_OFFSET_POS"); + // Since execution-specs #3278 the result is entirely fixed-size, so + // RESULT_FIXED_LEN is the exact encoded length and both remaining fields are + // direct reads — there is no chain_config offset to dereference. let fixed_len = sol_uint_const(&src, "RESULT_FIXED_LEN"); - let cc_off = u32_le(&buf, cc_off_pos); assert_eq!( - cc_off, fixed_len, - "chain_config data must start at RESULT_FIXED_LEN" + buf.len(), + fixed_len, + "RESULT_FIXED_LEN must equal the exact encoded result length" ); - // chain_id is chain_config's first field (uint64 LE) at the dereferenced offset. - let chain_id = u64::from_le_bytes(buf[cc_off..cc_off + 8].try_into().unwrap()); + + let chain_id_off = sol_uint_const(&src, "RESULT_CHAIN_ID_OFFSET"); + let chain_id = u64::from_le_bytes(buf[chain_id_off..chain_id_off + 8].try_into().unwrap()); assert_eq!( chain_id, 0x1122334455667788, - "chain_id must be readable at the RESULT_CHAIN_CONFIG_OFFSET_POS deref" + "RESULT_CHAIN_ID_OFFSET does not point at chain_id" + ); + + let schema_off = sol_uint_const(&src, "RESULT_SCHEMA_ID_OFFSET"); + let schema_id = u16::from_le_bytes(buf[schema_off..schema_off + 2].try_into().unwrap()); + assert_eq!( + schema_id, + ethrex_common::types::stateless_ssz::STATELESS_INPUT_SCHEMA_ID, + "RESULT_SCHEMA_ID_OFFSET does not point at schema_id" ); } From 5d2c1145af26aa50e278ff9c40032db5ce8ca1d0 Mon Sep 17 00:00:00 2001 From: Lucas Fiegl Date: Tue, 4 Aug 2026 17:54:29 -0300 Subject: [PATCH 11/30] feat(l1): prefix EXECUTE input with schema id --- .../src/nativeRollup/l1/NativeRollup.sol | 17 +++++++++++-- .../l2/sequencer/native_rollup/l1_advancer.rs | 10 ++++++-- crates/vm/levm/src/execute_precompile.rs | 25 ++++++++++++++++--- test/tests/l2/native_rollup_sol_offsets.rs | 18 +++++++++++-- test/tests/l2/ssz_round_trip.rs | 19 +++++++++++--- 5 files changed, 77 insertions(+), 12 deletions(-) diff --git a/crates/l2/contracts/src/nativeRollup/l1/NativeRollup.sol b/crates/l2/contracts/src/nativeRollup/l1/NativeRollup.sol index feb79ccc3db..ee24c03391c 100644 --- a/crates/l2/contracts/src/nativeRollup/l1/NativeRollup.sol +++ b/crates/l2/contracts/src/nativeRollup/l1/NativeRollup.sol @@ -110,6 +110,11 @@ contract NativeRollup { // revision 0x01. The guest echoes it so we can pin which rules were applied. uint16 constant EXPECTED_SCHEMA_ID = 0x1501; + // `statelessInputBytes` is a 2-byte big-endian schema id followed by the SSZ + // body, so every absolute read into the body is shifted by this much. The + // SSZ fixed part of `SszStatelessInput` begins at this offset. + uint256 constant INPUT_SCHEMA_PREFIX_LEN = 2; + // Byte offsets inside the SSZ `ExecutionPayload` fixed prefix. uint256 constant EP_PARENT_HASH_OFFSET = 0; uint256 constant EP_STATE_ROOT_OFFSET = 52; @@ -361,8 +366,16 @@ contract NativeRollup { bytes32 parentBeaconBlockRoot ) { - require(sszInput.length >= 20, "SSZ: input too short"); - uint256 nprAbs = _readU32LECalldata(sszInput, 0); + // 22 = 2-byte schema prefix + the 20-byte SszStatelessInput fixed part + // (npr_off 4 | witness_off 4 | chain_id 8 | public_keys_off 4). + require(sszInput.length >= INPUT_SCHEMA_PREFIX_LEN + 20, "SSZ: input too short"); + require( + (uint16(uint8(sszInput[0])) << 8) | uint16(uint8(sszInput[1])) == EXPECTED_SCHEMA_ID, + "SSZ: unexpected schema id" + ); + // SSZ offsets are relative to the body, so rebase them past the prefix. + uint256 nprAbs = + INPUT_SCHEMA_PREFIX_LEN + _readU32LECalldata(sszInput, INPUT_SCHEMA_PREFIX_LEN); // 44 = NPR fixed prefix: 3 var-field offsets (12) + parent_beacon_block_root (32). require(sszInput.length >= nprAbs + 44, "SSZ: NPR offset out of range"); parentBeaconBlockRoot = _readBytes32Calldata(sszInput, nprAbs + 8); diff --git a/crates/l2/sequencer/native_rollup/l1_advancer.rs b/crates/l2/sequencer/native_rollup/l1_advancer.rs index d225978add1..4aaa9fb3d23 100644 --- a/crates/l2/sequencer/native_rollup/l1_advancer.rs +++ b/crates/l2/sequencer/native_rollup/l1_advancer.rs @@ -369,8 +369,14 @@ pub fn build_ssz_stateless_input( public_keys: SszList::new(), // Empty for now }; - // 5. Serialize to SSZ bytes - let mut buf = Vec::new(); + // 5. Serialize to schema-prefixed SSZ bytes. + // + // The 2-byte big-endian schema id goes first, making this byte-identical to + // the spec's `statelessInputBytes`. Since #3278 it is the only carrier of the + // fork, so it is not optional framing. + let mut buf = ethrex_common::types::stateless_ssz::STATELESS_INPUT_SCHEMA_ID + .to_be_bytes() + .to_vec(); stateless_input.ssz_append(&mut buf); Ok(buf) } diff --git a/crates/vm/levm/src/execute_precompile.rs b/crates/vm/levm/src/execute_precompile.rs index 8fd1647b181..e78d6e6cc57 100644 --- a/crates/vm/levm/src/execute_precompile.rs +++ b/crates/vm/levm/src/execute_precompile.rs @@ -59,11 +59,30 @@ fn run_execute( calldata: &Bytes, gas_remaining: &mut u64, ) -> Result { - use ethrex_common::types::stateless_ssz::SszStatelessInput; + use ethrex_common::types::stateless_ssz::{ + STATELESS_INPUT_SCHEMA_ID, STATELESS_INPUT_SCHEMA_ID_SIZE, SszStatelessInput, + }; use libssz::SszDecode; - // Attacker-controlled input: SSZ decode failure is a CALL-level failure, not an invariant. - let input = SszStatelessInput::from_ssz_bytes(calldata) + // The input is schema-prefixed `statelessInputBytes`: a 2-byte big-endian + // schema id, then the SSZ body. Since execution-specs #3278 the prefix is the + // only thing that identifies the fork — no chain configuration crosses the + // wire — so rejecting an unexpected id is a correctness requirement, not a + // sanity check. Upstream `deserialize_stateless_input` rejects it the same way. + // + // All of this is attacker-controlled, so every failure here is a CALL-level + // failure rather than an invariant violation. + // `split_first_chunk` yields a fixed-size array, so no indexing is needed — + // this crate denies `clippy::indexing_slicing`. + let (schema_bytes, body) = calldata + .split_first_chunk::() + .ok_or(VMError::from(PrecompileError::ExecuteInvalidInput))?; + let schema_id = u16::from_be_bytes(*schema_bytes); + if schema_id != STATELESS_INPUT_SCHEMA_ID { + return Err(VMError::from(PrecompileError::ExecuteInvalidInput)); + } + + let input = SszStatelessInput::from_ssz_bytes(body) .map_err(|_| VMError::from(PrecompileError::ExecuteInvalidInput))?; validate_l2_constraints(&input)?; diff --git a/test/tests/l2/native_rollup_sol_offsets.rs b/test/tests/l2/native_rollup_sol_offsets.rs index 424c74f1921..0e41d551cad 100644 --- a/test/tests/l2/native_rollup_sol_offsets.rs +++ b/test/tests/l2/native_rollup_sol_offsets.rs @@ -88,6 +88,10 @@ fn sample_execution_payload() -> ExecutionPayload { } } +/// Encode a sample `statelessInputBytes`: the 2-byte big-endian schema id then +/// the SSZ body, exactly as `build_ssz_stateless_input` emits it. The prefix is +/// modelled here on purpose — the contract rebases every absolute read past it, +/// so a guard over an unprefixed body would not catch a framing mismatch. fn encode_sample_input() -> Vec { let input = SszStatelessInput { new_payload_request: NewPayloadRequest { @@ -110,7 +114,9 @@ fn encode_sample_input() -> Vec { chain_id: 1, public_keys: vec![].try_into().expect("public_keys"), }; - let mut buf = Vec::new(); + let mut buf = ethrex_common::types::stateless_ssz::STATELESS_INPUT_SCHEMA_ID + .to_be_bytes() + .to_vec(); input.ssz_append(&mut buf); buf } @@ -121,9 +127,17 @@ fn encode_sample_input() -> Vec { fn sol_ep_offsets_match_encoding() { let src = read_contract(); let buf = encode_sample_input(); + let prefix_len = sol_uint_const(&src, "INPUT_SCHEMA_PREFIX_LEN"); + assert_eq!(prefix_len, 2, "INPUT_SCHEMA_PREFIX_LEN must be 2"); + assert_eq!( + u16::from_be_bytes([buf[0], buf[1]]), + ethrex_common::types::stateless_ssz::STATELESS_INPUT_SCHEMA_ID, + "sample input must carry the schema-id prefix the contract requires" + ); // StatelessInput fixed part: 4 offsets; new_payload_request is field 0. - let npr_abs = u32_le(&buf, 0); + // SSZ offsets are body-relative, so rebase past the prefix as `advance()` does. + let npr_abs = prefix_len + u32_le(&buf, prefix_len); // NewPayloadRequest fixed prefix: execution_payload offset @ npr_abs. let ep_abs = npr_abs + u32_le(&buf, npr_abs); diff --git a/test/tests/l2/ssz_round_trip.rs b/test/tests/l2/ssz_round_trip.rs index 9ff5a0d47fb..552b68c3f94 100644 --- a/test/tests/l2/ssz_round_trip.rs +++ b/test/tests/l2/ssz_round_trip.rs @@ -7,7 +7,9 @@ use bytes::Bytes; use ethrex_common::types::block_execution_witness::ExecutionWitness; -use ethrex_common::types::stateless_ssz::SszStatelessInput; +use ethrex_common::types::stateless_ssz::{ + STATELESS_INPUT_SCHEMA_ID, STATELESS_INPUT_SCHEMA_ID_SIZE, SszStatelessInput, +}; use ethrex_common::types::{BlockBody, BlockHeader}; use ethrex_common::{Address, H256}; use ethrex_crypto::NativeCrypto; @@ -83,8 +85,19 @@ fn block_to_ssz_to_block_preserves_hash() { let ssz_bytes = build_ssz_stateless_input(&header, &body, &witness, None).expect("SSZ encoding failed"); - // SSZ → deserialize - let input = SszStatelessInput::from_ssz_bytes(&ssz_bytes).expect("SSZ decoding failed"); + // SSZ → deserialize. `build_ssz_stateless_input` emits schema-prefixed + // `statelessInputBytes` since execution-specs #3278, so strip and check the + // 2-byte schema id before decoding the body — the same order the EXECUTE + // precompile uses. + let (schema_bytes, body_bytes) = ssz_bytes + .split_first_chunk::() + .expect("input carries a schema-id prefix"); + assert_eq!( + u16::from_be_bytes(*schema_bytes), + STATELESS_INPUT_SCHEMA_ID, + "producer must emit the Amsterdam schema id" + ); + let input = SszStatelessInput::from_ssz_bytes(body_bytes).expect("SSZ decoding failed"); // SSZ → Block let reconstructed_block = From 8176e23427aada6e75804506013dc23e34c1bdbe Mon Sep 17 00:00:00 2001 From: Lucas Fiegl Date: Wed, 5 Aug 2026 09:18:57 -0300 Subject: [PATCH 12/30] fix(l1): hoist public-key check, unconditional libssz --- crates/guest-program/Cargo.toml | 15 +++--- crates/guest-program/src/l1/mod.rs | 1 + crates/guest-program/src/l1/program.rs | 71 ++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 8 deletions(-) diff --git a/crates/guest-program/Cargo.toml b/crates/guest-program/Cargo.toml index 70ca9fcaae0..e004524fc95 100644 --- a/crates/guest-program/Cargo.toml +++ b/crates/guest-program/Cargo.toml @@ -21,10 +21,13 @@ ethrex-rlp = { path = "../common/rlp", default-features = false } ethrex-l2-common = { path = "../l2/common", default-features = false } # EIP-8025 SSZ dependencies (optional) -libssz = { workspace = true, optional = true } -libssz-merkle = { workspace = true, optional = true } -libssz-types = { workspace = true, optional = true } -libssz-derive = { workspace = true, optional = true } +# Non-optional: the stateless-validation containers are unconditional since the +# SSZ types moved to ethrex-common, and the shared validation helpers here use them +# regardless of the (now-vestigial) eip-8025 feature. +libssz = { workspace = true } +libssz-merkle = { workspace = true } +libssz-types = { workspace = true } +libssz-derive = { workspace = true } # zkVM crypto dependencies (pure Rust, compile for RISC-V targets) k256 = { workspace = true, optional = true } @@ -74,10 +77,6 @@ l2 = [] eip-8025 = [ "ethrex-common/eip-8025", "ethrex-vm/eip-8025", - "dep:libssz", - "dep:libssz-merkle", - "dep:libssz-types", - "dep:libssz-derive", ] c-kzg = ["ethrex-vm/c-kzg", "ethrex-common/c-kzg"] secp256k1 = [ diff --git a/crates/guest-program/src/l1/mod.rs b/crates/guest-program/src/l1/mod.rs index 76082923c97..0f2010c960a 100644 --- a/crates/guest-program/src/l1/mod.rs +++ b/crates/guest-program/src/l1/mod.rs @@ -14,6 +14,7 @@ pub use input::{ProgramInputDecodeError, ProgramInputEncodeError}; pub use output::ProgramOutput; pub use program::execution_program; pub use program::new_payload_request_to_block; +pub use program::validate_public_keys; pub use program::verify_stateless_block; #[cfg(feature = "eip-8025")] pub use program::{ diff --git a/crates/guest-program/src/l1/program.rs b/crates/guest-program/src/l1/program.rs index 5f26b125988..13c03cf2cea 100644 --- a/crates/guest-program/src/l1/program.rs +++ b/crates/guest-program/src/l1/program.rs @@ -527,6 +527,77 @@ pub fn new_payload_request_to_block( /// Core stateless block validation for the native-rollup EXECUTE path. /// /// Sole caller: `ethrex-blockchain`'s `verify_stateless_new_payload` +/// Validate the per-transaction public keys carried by a stateless input. +/// +/// The spec's `StatelessInput` supplies one uncompressed secp256k1 key per +/// transaction so a guest can skip `ecrecover`; a key that does not derive to +/// the recovered sender must reject the payload (issue #6716). +/// +/// Hoisted out of the `eip8025_ssz` validation family, which was the only place +/// this check existed. Two consequences worth being explicit about: +/// +/// 1. The **guest** path must call this, or deduplicating the two validation +/// families silently drops a spec-required check. +/// 2. The **EXECUTE precompile** path ([`verify_stateless_block`]) deliberately +/// does *not* call it, and never has. `build_ssz_stateless_input` in the L2 +/// advancer sends `public_keys` empty ("Empty for now") while L2 blocks do +/// carry transactions, so enforcing the length check there would reject every +/// native-rollup block. Enabling it requires the producer to populate real +/// keys first. +/// +/// TODO(#6716): populate `public_keys` in the native-rollup producer and call +/// this from `verify_stateless_block`, so both paths enforce the spec. +/// +/// Upstream `build_stateless_input` *skips* keys for undecodable or +/// bad-signature transactions, so `public_keys.len()` can legitimately be +/// shorter than the transaction count. Such payloads are invalid on both sides +/// and both emit `successful_validation = false`, so the strict length check +/// stays output-compatible with the reference — but it is compared before any +/// zip so a short list fails cleanly rather than panicking. +/// Generic over the SSZ list bounds so it serves both the guest's +/// `PublicKeysList` and `stateless_ssz`'s field without a feature gate or a +/// duplicated alias. +pub fn validate_public_keys( + public_keys: &libssz_types::SszList, MAX_KEYS>, + block: ðrex_common::types::Block, + crypto: &dyn Crypto, +) -> Result<(), ExecutionError> { + if public_keys.len() != block.body.transactions.len() { + return Err(ExecutionError::Internal(format!( + "Found {} public keys in the stateless input, but there are {} transactions", + public_keys.len(), + block.body.transactions.len() + ))); + } + for (public_key, tx) in public_keys.iter().zip(block.body.transactions.iter()) { + // SSZ decode fixes the length at 65; uncompressed secp256k1 is 0x04 || X || Y. + let pk_bytes: &[u8] = public_key; + let Some((tag, xy)) = pk_bytes.split_first() else { + return Err(ExecutionError::Internal( + "Stateless input public key is empty".to_string(), + )); + }; + if *tag != 0x04 { + return Err(ExecutionError::Internal( + "Stateless input public key is not a 65-byte uncompressed secp256k1 key" + .to_string(), + )); + } + let hashed = ethrex_common::utils::keccak(xy); + let derived = ethrex_common::Address::from_slice(&hashed[12..]); + let recovered = tx.sender(crypto).map_err(|e| { + ExecutionError::Internal(format!("failed to recover transaction sender: {e}")) + })?; + if recovered != derived { + return Err(ExecutionError::Internal( + "Stateless input public key does not match recovered transaction sender" + .to_string(), + )); + } + } + Ok(()) +} + /// (`StatelessExecutor`, the `StatelessValidator` trait impl invoked by the /// EXECUTE precompile). NOTE: the zkVM guest binaries do **not** call this — /// they validate via the separate `validate_eip8025_*` path From 2d2b62bff1496526c9758c31cdb547e5541a3783 Mon Sep 17 00:00:00 2001 From: Lucas Fiegl Date: Wed, 5 Aug 2026 10:01:17 -0300 Subject: [PATCH 13/30] refactor(l1): remove the eip-8025 feature flag --- .github/workflows/pr-main_l2.yaml | 2 - Cargo.lock | 1 + crates/blockchain/stateless.rs | 4 +- crates/common/Cargo.toml | 1 - crates/common/trie/Cargo.toml | 1 - crates/common/types/block.rs | 2 +- crates/common/types/eip8025_ssz.rs | 462 ------------ crates/common/types/mod.rs | 6 +- crates/common/types/stateless_ssz.rs | 200 +---- crates/common/types/transaction.rs | 2 +- .../types/{eip8025_cell.rs => unsync_cell.rs} | 0 crates/guest-program/Cargo.toml | 6 +- crates/guest-program/bin/openvm/Cargo.toml | 1 - crates/guest-program/bin/openvm/src/main.rs | 28 +- crates/guest-program/bin/risc0/Cargo.toml | 1 - crates/guest-program/bin/risc0/src/main.rs | 24 +- crates/guest-program/bin/sp1/Cargo.toml | 1 - crates/guest-program/bin/sp1/src/main.rs | 23 +- crates/guest-program/bin/zisk/Cargo.toml | 1 - crates/guest-program/bin/zisk/src/main.rs | 23 +- crates/guest-program/src/l1/input.rs | 413 ++--------- crates/guest-program/src/l1/mod.rs | 20 +- crates/guest-program/src/l1/output.rs | 63 -- crates/guest-program/src/l1/program.rs | 700 +++--------------- crates/guest-program/src/l2/mod.rs | 2 +- crates/guest-program/src/l2/program.rs | 13 + crates/guest-program/src/lib.rs | 20 +- crates/prover/Cargo.toml | 2 +- crates/prover/src/backend/exec.rs | 59 +- crates/prover/src/backend/openvm.rs | 9 +- crates/prover/src/backend/risc0.rs | 7 + crates/prover/src/backend/sp1.rs | 9 +- crates/prover/src/backend/zisk.rs | 6 + crates/prover/src/lib.rs | 13 - crates/vm/Cargo.toml | 1 - tooling/Cargo.lock | 39 +- tooling/ef_tests/blockchain/Cargo.toml | 10 +- tooling/ef_tests/blockchain/Makefile | 5 +- tooling/ef_tests/blockchain/test_runner.rs | 137 ++-- tooling/ef_tests/blockchain/tests/all.rs | 37 +- 40 files changed, 382 insertions(+), 1972 deletions(-) delete mode 100644 crates/common/types/eip8025_ssz.rs rename crates/common/types/{eip8025_cell.rs => unsync_cell.rs} (100%) delete mode 100644 crates/guest-program/src/l1/output.rs diff --git a/.github/workflows/pr-main_l2.yaml b/.github/workflows/pr-main_l2.yaml index 5ef60311676..6f6a923bc24 100644 --- a/.github/workflows/pr-main_l2.yaml +++ b/.github/workflows/pr-main_l2.yaml @@ -103,8 +103,6 @@ jobs: - name: Run cargo check run: cargo check --workspace --features l2,l2-sql - - name: Run cargo check (eip-8025 SSZ guest path) - run: cargo check --workspace --features eip-8025 - name: Run cargo clippy run: | diff --git a/Cargo.lock b/Cargo.lock index 86fc23ce4a3..56a4a23d31c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4280,6 +4280,7 @@ dependencies = [ "ethrex-guest-program", "ethrex-rlp", "ethrex-vm", + "libssz", "openvm-continuations", "openvm-sdk", "openvm-stark-sdk 1.2.1", diff --git a/crates/blockchain/stateless.rs b/crates/blockchain/stateless.rs index f8777bc619b..688d0c467b5 100644 --- a/crates/blockchain/stateless.rs +++ b/crates/blockchain/stateless.rs @@ -4,8 +4,8 @@ //! (projects/zkevm branch). It is invoked by the EXECUTE precompile via the //! `StatelessValidator` trait (see `StatelessExecutor` below). //! -//! Note: the zkVM guest binaries validate via the separate `validate_eip8025_*` -//! path, not this module. +//! Note: the zkVM guest binaries reach this same code through +//! `l1::run_stateless_guest`; there is no longer a separate validation family. use std::sync::Arc; diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 9e8466cf807..9a911bd16df 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -54,7 +54,6 @@ default = ["secp256k1", "rayon"] c-kzg = ["ethrex-crypto/c-kzg"] rayon = ["dep:rayon"] secp256k1 = ["dep:secp256k1", "ethrex-crypto/secp256k1"] -eip-8025 = ["ethrex-trie/eip-8025"] risc0 = ["ethrex-crypto/risc0"] sp1 = [] diff --git a/crates/common/trie/Cargo.toml b/crates/common/trie/Cargo.toml index 56dea584af2..090a02ce626 100644 --- a/crates/common/trie/Cargo.toml +++ b/crates/common/trie/Cargo.toml @@ -48,7 +48,6 @@ std = [ "dep:crossbeam", "dep:rayon", ] -eip-8025 = [] [lib] path = "./trie.rs" diff --git a/crates/common/types/block.rs b/crates/common/types/block.rs index afaa303079f..b805a6144ef 100644 --- a/crates/common/types/block.rs +++ b/crates/common/types/block.rs @@ -28,7 +28,7 @@ pub type BlockNumber = u64; pub type BlockHash = H256; #[cfg(all(feature = "zisk", target_arch = "riscv64"))] -use super::eip8025_cell::OnceCell; +use super::unsync_cell::OnceCell; #[cfg(not(all(feature = "zisk", target_arch = "riscv64")))] use once_cell::sync::OnceCell; diff --git a/crates/common/types/eip8025_ssz.rs b/crates/common/types/eip8025_ssz.rs deleted file mode 100644 index e4adbbb3901..00000000000 --- a/crates/common/types/eip8025_ssz.rs +++ /dev/null @@ -1,462 +0,0 @@ -//! SSZ containers for EIP-8025 (Execution Layer Triggerable Proofs). -//! -//! These types mirror the CL-side SSZ definitions used for tree-hashing -//! `NewPayloadRequest` and producing the `PublicInput` committed to by -//! execution proofs. - -use bytes::Bytes; -use libssz::{SszDecode, SszEncode}; -use libssz_derive::{HashTreeRoot, SszDecode, SszEncode}; -use libssz_merkle::{HashTreeRoot, Sha256Hasher}; -use libssz_types::{SszList, SszVector}; - -use super::requests::EncodedRequests; - -// ── Spec limits (Electra) ────────────────────────────────────────── - -/// `MAX_TRANSACTIONS_PER_PAYLOAD` (Electra). -const MAX_TRANSACTIONS_PER_PAYLOAD: usize = 1_048_576; -/// `MAX_WITHDRAWALS_PER_PAYLOAD` (Electra). -const MAX_WITHDRAWALS_PER_PAYLOAD: usize = 16; -/// `MAX_BYTES_PER_TRANSACTION`. -const MAX_BYTES_PER_TRANSACTION: usize = 1_073_741_824; -/// `MAX_EXTRA_DATA_BYTES`. -const MAX_EXTRA_DATA_BYTES: usize = 32; -/// `MAX_DEPOSIT_REQUESTS_PER_PAYLOAD` (Electra). -const MAX_DEPOSIT_REQUESTS_PER_PAYLOAD: usize = 8192; -/// `MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD` (Electra). -const MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: usize = 16; -/// `MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD` (Electra). -const MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: usize = 2; -/// `MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD` (EIP-8282, `2**6`). -const MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD: usize = 64; -/// `MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD` (EIP-8282, `2**4`). -const MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD: usize = 16; -/// `MAX_BLOB_COMMITMENTS_PER_BLOCK` (Electra). -const MAX_BLOB_COMMITMENTS_PER_BLOCK: usize = 4096; - -// ── EIP-7685 request type prefixes ───────────────────────────────── - -const DEPOSIT_REQUEST_TYPE: u8 = 0x00; -const WITHDRAWAL_REQUEST_TYPE: u8 = 0x01; -const CONSOLIDATION_REQUEST_TYPE: u8 = 0x02; -const BUILDER_DEPOSIT_REQUEST_TYPE: u8 = 0x03; -const BUILDER_EXIT_REQUEST_TYPE: u8 = 0x04; - -// ── Bytes20 wrapper (address) ────────────────────────────────────── -// -// libssz implements `SszEncode`/`SszDecode` for `[u8; 20]` but NOT -// `HashTreeRoot`. Per the SSZ spec, a 20-byte basic value is -// right-padded with zeros to 32 bytes for its tree hash leaf. - -/// A 20-byte value (e.g. an execution address) with SSZ + HTR support. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(transparent)] -pub struct Bytes20(pub [u8; 20]); - -impl SszEncode for Bytes20 { - fn is_fixed_size() -> bool { - true - } - fn fixed_size() -> usize { - 20 - } - fn encoded_len(&self) -> usize { - 20 - } - fn ssz_append(&self, buf: &mut Vec) { - self.0.ssz_append(buf); - } -} - -impl SszDecode for Bytes20 { - fn is_fixed_size() -> bool { - true - } - fn fixed_size() -> usize { - 20 - } - fn from_ssz_bytes(bytes: &[u8]) -> Result { - <[u8; 20]>::from_ssz_bytes(bytes).map(Self) - } -} - -impl HashTreeRoot for Bytes20 { - fn hash_tree_root(&self, _hasher: &impl Sha256Hasher) -> libssz_merkle::Node { - let mut node = [0u8; 32]; - node[..20].copy_from_slice(&self.0); - node - } -} - -impl From<[u8; 20]> for Bytes20 { - fn from(bytes: [u8; 20]) -> Self { - Self(bytes) - } -} - -impl From for [u8; 20] { - fn from(b: Bytes20) -> Self { - b.0 - } -} - -// ── LogsBloom type alias ─────────────────────────────────────────── -// -// `logs_bloom` is `ByteVector[BYTES_PER_LOGS_BLOOM]` in the CL spec — -// a fixed-length SSZ vector of 256 bytes. - -/// `BYTES_PER_LOGS_BLOOM` from the CL spec. -pub const BYTES_PER_LOGS_BLOOM: usize = 256; - -/// `ByteVector[256]` — the logs bloom as a fixed-size SSZ vector. -pub type LogsBloom = SszVector; - -// ── Sub-containers ───────────────────────────────────────────────── - -/// SSZ `Withdrawal` container matching the CL spec. -#[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode, HashTreeRoot)] -pub struct Withdrawal { - pub index: u64, - pub validator_index: u64, - pub address: Bytes20, - pub amount: u64, -} - -/// SSZ `DepositRequest` container (EIP-6110). -#[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode, HashTreeRoot)] -pub struct DepositRequest { - pub pubkey: [u8; 48], - pub withdrawal_credentials: [u8; 32], - pub amount: u64, - pub signature: [u8; 96], - pub index: u64, -} - -/// SSZ `WithdrawalRequest` container (EIP-7002). -#[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode, HashTreeRoot)] -pub struct WithdrawalRequest { - pub source_address: Bytes20, - pub validator_pubkey: [u8; 48], - pub amount: u64, -} - -/// SSZ `ConsolidationRequest` container (EIP-7251). -#[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode, HashTreeRoot)] -pub struct ConsolidationRequest { - pub source_address: Bytes20, - pub source_pubkey: [u8; 48], - pub target_pubkey: [u8; 48], -} - -/// SSZ `BuilderDepositRequest` container (EIP-8282). -#[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode, HashTreeRoot)] -pub struct BuilderDepositRequest { - pub pubkey: [u8; 48], - pub withdrawal_credentials: [u8; 32], - pub amount: u64, - pub signature: [u8; 96], -} - -/// SSZ `BuilderExitRequest` container (EIP-8282). -#[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode, HashTreeRoot)] -pub struct BuilderExitRequest { - pub source_address: Bytes20, - pub pubkey: [u8; 48], -} - -// ── ExecutionPayload ─────────────────────────────────────────────── - -/// SSZ `ExecutionPayload` container matching `ExecutionPayloadElectra` from -/// the consensus spec. -#[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode, HashTreeRoot)] -pub struct ExecutionPayload { - pub parent_hash: [u8; 32], - pub fee_recipient: Bytes20, - pub state_root: [u8; 32], - pub receipts_root: [u8; 32], - pub logs_bloom: LogsBloom, - pub prev_randao: [u8; 32], - pub block_number: u64, - pub gas_limit: u64, - pub gas_used: u64, - pub timestamp: u64, - pub extra_data: SszList, - /// `base_fee_per_gas` encoded as a 256-bit unsigned integer (little-endian). - pub base_fee_per_gas: [u8; 32], - pub block_hash: [u8; 32], - pub transactions: SszList, MAX_TRANSACTIONS_PER_PAYLOAD>, - pub withdrawals: SszList, - pub blob_gas_used: u64, - pub excess_blob_gas: u64, -} - -/// SSZ `ExecutionPayload` execution payload V4. -#[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode, HashTreeRoot)] -pub struct ExecutionPayloadV4 { - pub parent_hash: [u8; 32], - pub fee_recipient: Bytes20, - pub state_root: [u8; 32], - pub receipts_root: [u8; 32], - pub logs_bloom: LogsBloom, - pub prev_randao: [u8; 32], - pub block_number: u64, - pub gas_limit: u64, - pub gas_used: u64, - pub timestamp: u64, - pub extra_data: SszList, - /// `base_fee_per_gas` encoded as a 256-bit unsigned integer (little-endian). - pub base_fee_per_gas: [u8; 32], - pub block_hash: [u8; 32], - pub transactions: SszList, MAX_TRANSACTIONS_PER_PAYLOAD>, - pub withdrawals: SszList, - pub blob_gas_used: u64, - pub excess_blob_gas: u64, - pub block_access_list: SszList, - pub slot_number: u64, -} - -// ── ExecutionRequests ────────────────────────────────────────────── - -/// SSZ `ExecutionRequests` container (Electra) — the typed EIP-7685 bundle -/// that the CL commits to alongside `ExecutionPayload`. -#[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode, HashTreeRoot)] -pub struct ExecutionRequests { - pub deposits: SszList, - pub withdrawals: SszList, - pub consolidations: SszList, - pub builder_deposits: SszList, - pub builder_exits: SszList, -} - -impl ExecutionRequests { - /// Produce the EIP-7685 encoded form: five `EncodedRequests` entries, - /// one per request type, each `[type_byte] ++ concat(ssz_encode(item))`. - /// - /// The five request types are all fixed-size SSZ containers, so their - /// SSZ encoding is byte-for-byte the EL wire concatenation that - /// `compute_requests_hash` expects. - pub fn to_encoded_requests(&self) -> Vec { - fn encode( - type_byte: u8, - items: impl IntoIterator, - ) -> EncodedRequests { - let mut buf = Vec::new(); - buf.push(type_byte); - for item in items { - item.ssz_append(&mut buf); - } - EncodedRequests(Bytes::from(buf)) - } - - vec![ - encode(DEPOSIT_REQUEST_TYPE, self.deposits.iter().cloned()), - encode(WITHDRAWAL_REQUEST_TYPE, self.withdrawals.iter().cloned()), - encode( - CONSOLIDATION_REQUEST_TYPE, - self.consolidations.iter().cloned(), - ), - encode( - BUILDER_DEPOSIT_REQUEST_TYPE, - self.builder_deposits.iter().cloned(), - ), - encode( - BUILDER_EXIT_REQUEST_TYPE, - self.builder_exits.iter().cloned(), - ), - ] - } -} - -// ── NewPayloadRequest ────────────────────────────────────────────── - -/// SSZ `NewPayloadRequest` — the key container whose `hash_tree_root` is -/// the public input committed to by an execution proof. -#[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode, HashTreeRoot)] -pub struct NewPayloadRequest { - pub execution_payload: ExecutionPayload, - pub versioned_hashes: SszList<[u8; 32], MAX_BLOB_COMMITMENTS_PER_BLOCK>, - pub parent_beacon_block_root: [u8; 32], - pub execution_requests: ExecutionRequests, -} - -/// SSZ `NewPayloadRequest` for the Amsterdam fork. -#[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode, HashTreeRoot)] -pub struct NewPayloadRequestAmsterdam { - pub execution_payload: ExecutionPayloadV4, - pub versioned_hashes: SszList<[u8; 32], MAX_BLOB_COMMITMENTS_PER_BLOCK>, - pub parent_beacon_block_root: [u8; 32], - pub execution_requests: ExecutionRequests, -} - -// ── PublicInput ──────────────────────────────────────────────────── - -/// The public input for an execution proof: the `hash_tree_root` of the -/// `NewPayloadRequest`. -#[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode, HashTreeRoot)] -pub struct PublicInput { - pub new_payload_request_root: [u8; 32], -} - -impl NewPayloadRequest { - /// Compute the `hash_tree_root` of this request — the value that - /// becomes the execution proof's public input. - pub fn public_input(&self, hasher: &impl Sha256Hasher) -> PublicInput { - PublicInput { - new_payload_request_root: self.hash_tree_root(hasher), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use libssz_merkle::Sha2Hasher; - - const HASHER: Sha2Hasher = Sha2Hasher; - - fn sample_payload() -> ExecutionPayload { - ExecutionPayload { - parent_hash: [1u8; 32], - fee_recipient: Bytes20([2u8; 20]), - state_root: [3u8; 32], - receipts_root: [4u8; 32], - logs_bloom: vec![0u8; 256].try_into().expect("logs_bloom length"), - prev_randao: [5u8; 32], - block_number: 42, - gas_limit: 30_000_000, - gas_used: 21_000, - timestamp: 1_700_000_000, - extra_data: vec![0xAB, 0xCD].try_into().expect("extra_data fits"), - base_fee_per_gas: { - let mut b = [0u8; 32]; - b[0] = 7; // 7 in LE - b - }, - block_hash: [6u8; 32], - transactions: vec![ - vec![0xDE, 0xAD, 0xBE, 0xEF] - .try_into() - .expect("tx bytes fit"), - ] - .try_into() - .expect("txs fit"), - withdrawals: vec![Withdrawal { - index: 0, - validator_index: 1, - address: Bytes20([7u8; 20]), - amount: 1_000_000, - }] - .try_into() - .expect("withdrawals fit"), - blob_gas_used: 0, - excess_blob_gas: 0, - } - } - - fn empty_requests() -> ExecutionRequests { - ExecutionRequests { - deposits: vec![].try_into().expect("empty deposits"), - withdrawals: vec![].try_into().expect("empty withdrawals"), - consolidations: vec![].try_into().expect("empty consolidations"), - builder_deposits: vec![].try_into().expect("empty builder deposits"), - builder_exits: vec![].try_into().expect("empty builder exits"), - } - } - - fn sample_request() -> NewPayloadRequest { - NewPayloadRequest { - execution_payload: sample_payload(), - versioned_hashes: vec![].try_into().expect("empty versioned_hashes"), - parent_beacon_block_root: [8u8; 32], - execution_requests: empty_requests(), - } - } - - #[test] - fn test_ssz_root_changes_with_different_data() { - let request1 = sample_request(); - let mut request2 = sample_request(); - request2.execution_payload.block_number = 99; - - assert_ne!( - request1.hash_tree_root(&HASHER), - request2.hash_tree_root(&HASHER), - "Different payloads must produce different roots" - ); - } - - #[test] - fn test_ssz_root_is_deterministic() { - let request = sample_request(); - let root1 = request.hash_tree_root(&HASHER); - let root2 = request.hash_tree_root(&HASHER); - assert_eq!(root1, root2, "Same request must produce same root"); - } - - #[test] - fn test_execution_requests_to_encoded_bytes() { - let requests = ExecutionRequests { - deposits: vec![DepositRequest { - pubkey: [0x11; 48], - withdrawal_credentials: [0x22; 32], - amount: 32_000_000_000, - signature: [0x33; 96], - index: 7, - }] - .try_into() - .expect("one deposit fits"), - withdrawals: vec![WithdrawalRequest { - source_address: Bytes20([0x44; 20]), - validator_pubkey: [0x55; 48], - amount: 1_000_000, - }] - .try_into() - .expect("one withdrawal fits"), - consolidations: vec![ConsolidationRequest { - source_address: Bytes20([0x66; 20]), - source_pubkey: [0x77; 48], - target_pubkey: [0x88; 48], - }] - .try_into() - .expect("one consolidation fits"), - builder_deposits: vec![BuilderDepositRequest { - pubkey: [0x99; 48], - withdrawal_credentials: [0xAA; 32], - amount: 32_000_000_000, - signature: [0xBB; 96], - }] - .try_into() - .expect("one builder deposit fits"), - builder_exits: vec![BuilderExitRequest { - source_address: Bytes20([0xCC; 20]), - pubkey: [0xDD; 48], - }] - .try_into() - .expect("one builder exit fits"), - }; - - let encoded = requests.to_encoded_requests(); - assert_eq!(encoded.len(), 5, "must emit 5 EIP-7685 entries"); - - // Deposit: [0x00] ++ 192 bytes - assert_eq!(encoded[0].0[0], DEPOSIT_REQUEST_TYPE); - assert_eq!(encoded[0].0.len(), 1 + 192); - - // Withdrawal: [0x01] ++ 76 bytes - assert_eq!(encoded[1].0[0], WITHDRAWAL_REQUEST_TYPE); - assert_eq!(encoded[1].0.len(), 1 + 76); - - // Consolidation: [0x02] ++ 116 bytes - assert_eq!(encoded[2].0[0], CONSOLIDATION_REQUEST_TYPE); - assert_eq!(encoded[2].0.len(), 1 + 116); - - // Builder deposit: [0x03] ++ 184 bytes (48 + 32 + 8 + 96) - assert_eq!(encoded[3].0[0], BUILDER_DEPOSIT_REQUEST_TYPE); - assert_eq!(encoded[3].0.len(), 1 + 184); - - // Builder exit: [0x04] ++ 68 bytes (20 + 48) - assert_eq!(encoded[4].0[0], BUILDER_EXIT_REQUEST_TYPE); - assert_eq!(encoded[4].0.len(), 1 + 68); - } -} diff --git a/crates/common/types/mod.rs b/crates/common/types/mod.rs index 4126b5a090b..8a4d5c9fd27 100644 --- a/crates/common/types/mod.rs +++ b/crates/common/types/mod.rs @@ -5,10 +5,6 @@ mod block; pub mod block_access_list; pub mod block_execution_witness; mod constants; -#[cfg(all(feature = "zisk", target_arch = "riscv64"))] -pub(crate) mod eip8025_cell; -#[cfg(feature = "eip-8025")] -pub mod eip8025_ssz; mod fork_id; mod genesis; pub mod l2; @@ -19,6 +15,8 @@ pub mod requests; pub mod stateless_ssz; pub mod transaction; pub mod tx_fields; +#[cfg(all(feature = "zisk", target_arch = "riscv64"))] +pub(crate) mod unsync_cell; pub use account::*; pub use account_update::*; diff --git a/crates/common/types/stateless_ssz.rs b/crates/common/types/stateless_ssz.rs index e0b99788923..b62fcb52e1d 100644 --- a/crates/common/types/stateless_ssz.rs +++ b/crates/common/types/stateless_ssz.rs @@ -388,6 +388,13 @@ pub const STATELESS_INPUT_SCHEMA_ID: u16 = 0x1501; /// Byte length of the big-endian [`STATELESS_INPUT_SCHEMA_ID`] prefix. pub const STATELESS_INPUT_SCHEMA_ID_SIZE: usize = 2; +/// SSZ shape of `SszStatelessInput::public_keys`: one fixed-size 65-byte +/// uncompressed secp256k1 key per transaction. +/// +/// Aliased so consumers can name the type without restating the list bounds, and +/// so the bounds stay private to this module. +pub type SszPublicKeys = SszList, MAX_PUBLIC_KEYS>; + /// SSZ `ExecutionWitness` container matching the execution-specs definition. /// /// Contains all data needed for stateless execution: @@ -413,11 +420,11 @@ pub struct SszStatelessInput { /// activation info and blob schedules are guest-internal, keyed by /// `(chain_id, fork)`, with the fork coming from the schema-id prefix. pub chain_id: u64, - pub public_keys: SszList, MAX_PUBLIC_KEYS>, + pub public_keys: SszPublicKeys, } /// SSZ `StatelessValidationResult` — the output of `verify_stateless_new_payload`. -#[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode, HashTreeRoot)] +#[derive(Debug, Clone, Default, PartialEq, Eq, SszEncode, SszDecode, HashTreeRoot)] pub struct SszStatelessValidationResult { pub new_payload_request_root: [u8; 32], pub successful_validation: bool, @@ -871,193 +878,4 @@ mod tests { "slot_number must be readable at EP+532", ); } - - /// Equivalence guard for the duplicated SSZ wire-format definitions in - /// `stateless_ssz` vs `eip8025_ssz`. The two modules independently define - /// the same containers; they are byte-identical today, but nothing in the - /// type system forces them to stay that way. If someone edits one (field - /// order, a type, a `MAX_*` bound affecting offsets) without the other, the - /// EXECUTE precompile path (`stateless_ssz`) and the zk-guest path - /// (`eip8025_ssz`) would silently disagree on the wire format. This test - /// fails loudly when that happens by encoding equivalent values in both - /// modules and asserting the bytes match. Gated on `eip-8025` because - /// `eip8025_ssz` (the zk-guest path) only exists under that feature. - #[test] - #[cfg(feature = "eip-8025")] - fn stateless_ssz_matches_eip8025_ssz_wire_format() { - use crate::types::eip8025_ssz as e; - - fn enc(x: &T) -> Vec { - let mut b = Vec::new(); - x.ssz_append(&mut b); - b - } - - // Withdrawal - assert_eq!( - enc(&Withdrawal { - index: 1, - validator_index: 2, - address: Bytes20([3u8; 20]), - amount: 4, - }), - enc(&e::Withdrawal { - index: 1, - validator_index: 2, - address: e::Bytes20([3u8; 20]), - amount: 4, - }), - "Withdrawal wire format diverged between stateless_ssz and eip8025_ssz", - ); - - // DepositRequest - assert_eq!( - enc(&DepositRequest { - pubkey: [5u8; 48], - withdrawal_credentials: [6u8; 32], - amount: 7, - signature: [8u8; 96], - index: 9, - }), - enc(&e::DepositRequest { - pubkey: [5u8; 48], - withdrawal_credentials: [6u8; 32], - amount: 7, - signature: [8u8; 96], - index: 9, - }), - "DepositRequest wire format diverged", - ); - - // WithdrawalRequest - assert_eq!( - enc(&WithdrawalRequest { - source_address: Bytes20([10u8; 20]), - validator_pubkey: [11u8; 48], - amount: 12, - }), - enc(&e::WithdrawalRequest { - source_address: e::Bytes20([10u8; 20]), - validator_pubkey: [11u8; 48], - amount: 12, - }), - "WithdrawalRequest wire format diverged", - ); - - // ConsolidationRequest - assert_eq!( - enc(&ConsolidationRequest { - source_address: Bytes20([13u8; 20]), - source_pubkey: [14u8; 48], - target_pubkey: [15u8; 48], - }), - enc(&e::ConsolidationRequest { - source_address: e::Bytes20([13u8; 20]), - source_pubkey: [14u8; 48], - target_pubkey: [15u8; 48], - }), - "ConsolidationRequest wire format diverged", - ); - - // ExecutionRequests (one of each; exercises the offset layout too) - let reqs = ExecutionRequests { - deposits: vec![DepositRequest { - pubkey: [5u8; 48], - withdrawal_credentials: [6u8; 32], - amount: 7, - signature: [8u8; 96], - index: 9, - }] - .try_into() - .expect("deposits fit"), - withdrawals: vec![WithdrawalRequest { - source_address: Bytes20([10u8; 20]), - validator_pubkey: [11u8; 48], - amount: 12, - }] - .try_into() - .expect("withdrawals fit"), - consolidations: vec![ConsolidationRequest { - source_address: Bytes20([13u8; 20]), - source_pubkey: [14u8; 48], - target_pubkey: [15u8; 48], - }] - .try_into() - .expect("consolidations fit"), - }; - let e_reqs = e::ExecutionRequests { - deposits: vec![e::DepositRequest { - pubkey: [5u8; 48], - withdrawal_credentials: [6u8; 32], - amount: 7, - signature: [8u8; 96], - index: 9, - }] - .try_into() - .expect("deposits fit"), - withdrawals: vec![e::WithdrawalRequest { - source_address: e::Bytes20([10u8; 20]), - validator_pubkey: [11u8; 48], - amount: 12, - }] - .try_into() - .expect("withdrawals fit"), - consolidations: vec![e::ConsolidationRequest { - source_address: e::Bytes20([13u8; 20]), - source_pubkey: [14u8; 48], - target_pubkey: [15u8; 48], - }] - .try_into() - .expect("consolidations fit"), - }; - assert_eq!( - enc(&reqs), - enc(&e_reqs), - "ExecutionRequests wire format diverged", - ); - - // Full payload: stateless_ssz::ExecutionPayload must be byte-identical to - // eip8025_ssz::ExecutionPayloadV4 (both are the Electra payload + EIP-7928 - // block_access_list + EIP-7843 slot_number). - let ep = sample_payload(); - let e_ep = e::ExecutionPayloadV4 { - parent_hash: ep.parent_hash, - fee_recipient: e::Bytes20(ep.fee_recipient.0), - state_root: ep.state_root, - receipts_root: ep.receipts_root, - logs_bloom: vec![0u8; 256].try_into().expect("logs_bloom length"), - prev_randao: ep.prev_randao, - block_number: ep.block_number, - gas_limit: ep.gas_limit, - gas_used: ep.gas_used, - timestamp: ep.timestamp, - extra_data: vec![0xAB, 0xCD].try_into().expect("extra_data fits"), - base_fee_per_gas: ep.base_fee_per_gas, - block_hash: ep.block_hash, - transactions: vec![ - vec![0xDE, 0xAD, 0xBE, 0xEF] - .try_into() - .expect("tx bytes fit"), - ] - .try_into() - .expect("txs fit"), - withdrawals: vec![e::Withdrawal { - index: 0, - validator_index: 1, - address: e::Bytes20([7u8; 20]), - amount: 1_000_000, - }] - .try_into() - .expect("withdrawals fit"), - blob_gas_used: ep.blob_gas_used, - excess_blob_gas: ep.excess_blob_gas, - block_access_list: ProgressiveList::new(), - slot_number: ep.slot_number, - }; - assert_eq!( - enc(&ep), - enc(&e_ep), - "ExecutionPayload(V4) wire format diverged between stateless_ssz and eip8025_ssz", - ); - } } diff --git a/crates/common/types/transaction.rs b/crates/common/types/transaction.rs index 447f6a13b84..d2f9f73a06d 100644 --- a/crates/common/types/transaction.rs +++ b/crates/common/types/transaction.rs @@ -48,7 +48,7 @@ use ethrex_rlp::{ }; #[cfg(all(feature = "zisk", target_arch = "riscv64"))] -use super::eip8025_cell::OnceCell; +use super::unsync_cell::OnceCell; use crate::types::{ AccessList, AuthorizationList, BlobsBundle, constants::VERSIONED_HASH_VERSION_KZG, }; diff --git a/crates/common/types/eip8025_cell.rs b/crates/common/types/unsync_cell.rs similarity index 100% rename from crates/common/types/eip8025_cell.rs rename to crates/common/types/unsync_cell.rs diff --git a/crates/guest-program/Cargo.toml b/crates/guest-program/Cargo.toml index e004524fc95..b7dc4c3ab53 100644 --- a/crates/guest-program/Cargo.toml +++ b/crates/guest-program/Cargo.toml @@ -23,7 +23,7 @@ ethrex-l2-common = { path = "../l2/common", default-features = false } # EIP-8025 SSZ dependencies (optional) # Non-optional: the stateless-validation containers are unconditional since the # SSZ types moved to ethrex-common, and the shared validation helpers here use them -# regardless of the (now-vestigial) eip-8025 feature. +# regardless of build configuration. libssz = { workspace = true } libssz-merkle = { workspace = true } libssz-types = { workspace = true } @@ -74,10 +74,6 @@ zisk-build-elf = ["zisk"] openvm-build-elf = ["openvm"] l2 = [] -eip-8025 = [ - "ethrex-common/eip-8025", - "ethrex-vm/eip-8025", -] c-kzg = ["ethrex-vm/c-kzg", "ethrex-common/c-kzg"] secp256k1 = [ "ethrex-common/secp256k1", diff --git a/crates/guest-program/bin/openvm/Cargo.toml b/crates/guest-program/bin/openvm/Cargo.toml index 616c1b7493c..c44f4074750 100644 --- a/crates/guest-program/bin/openvm/Cargo.toml +++ b/crates/guest-program/bin/openvm/Cargo.toml @@ -29,4 +29,3 @@ ethrex-guest-program = { path = "../..", default-features = false, features = [ [features] l2 = ["ethrex-guest-program/l2"] -eip-8025 = ["ethrex-guest-program/eip-8025"] diff --git a/crates/guest-program/bin/openvm/src/main.rs b/crates/guest-program/bin/openvm/src/main.rs index ac245b52eba..bcc8d301939 100644 --- a/crates/guest-program/bin/openvm/src/main.rs +++ b/crates/guest-program/bin/openvm/src/main.rs @@ -1,11 +1,9 @@ use std::sync::Arc; #[cfg(feature = "l2")] -use ethrex_guest_program::l2::{ProgramInput, execution_program}; -#[cfg(all(not(feature = "l2"), not(feature = "eip-8025")))] -use ethrex_guest_program::l1::{ProgramInput, execution_program}; -#[cfg(all(not(feature = "l2"), feature = "eip-8025"))] -use ethrex_guest_program::l1::execution_program; +use ethrex_guest_program::l2::run_guest; +#[cfg(not(feature = "l2"))] +use ethrex_guest_program::l1::run_stateless_guest; use ethrex_guest_program::crypto::openvm::OpenVmCrypto; use openvm_keccak256::keccak256; @@ -15,28 +13,18 @@ openvm::init!(); pub fn main() { openvm::io::println("start reading input"); let input = openvm::io::read_vec(); - - #[cfg(not(feature = "eip-8025"))] - let input = { - use rkyv::rancor::Error; - rkyv::from_bytes::(&input).unwrap() - }; openvm::io::println("finish reading input"); let crypto = Arc::new(OpenVmCrypto); openvm::io::println("start execution"); - #[cfg(feature = "eip-8025")] - let output = execution_program(&input, crypto).unwrap(); - #[cfg(not(feature = "eip-8025"))] - let output = execution_program(input, crypto).unwrap(); + #[cfg(not(feature = "l2"))] + let output = run_stateless_guest(&input, crypto); + #[cfg(feature = "l2")] + let output = run_guest(&input, crypto).unwrap(); openvm::io::println("finish execution"); - openvm::io::println("start hashing output"); - let output = keccak256(&output.encode()); - openvm::io::println("finish hashing output"); - openvm::io::println("start revealing output"); - openvm::io::reveal_bytes32(output); + openvm::io::reveal_bytes32(keccak256(&output)); openvm::io::println("finish revealing output"); } diff --git a/crates/guest-program/bin/risc0/Cargo.toml b/crates/guest-program/bin/risc0/Cargo.toml index 5440ab477ad..6190331d34a 100644 --- a/crates/guest-program/bin/risc0/Cargo.toml +++ b/crates/guest-program/bin/risc0/Cargo.toml @@ -48,4 +48,3 @@ substrate-bn = { git = "https://github.com/risc0/paritytech-bn", tag = "v0.6.0-r [features] l2 = ["ethrex-guest-program/l2"] -eip-8025 = ["ethrex-guest-program/eip-8025"] diff --git a/crates/guest-program/bin/risc0/src/main.rs b/crates/guest-program/bin/risc0/src/main.rs index eb4e19222a8..3e76912487f 100644 --- a/crates/guest-program/bin/risc0/src/main.rs +++ b/crates/guest-program/bin/risc0/src/main.rs @@ -2,11 +2,9 @@ use std::io::Read; use std::sync::Arc; #[cfg(feature = "l2")] -use ethrex_guest_program::l2::{ProgramInput, execution_program}; -#[cfg(all(not(feature = "l2"), not(feature = "eip-8025")))] -use ethrex_guest_program::l1::{ProgramInput, execution_program}; -#[cfg(all(not(feature = "l2"), feature = "eip-8025"))] -use ethrex_guest_program::l1::execution_program; +use ethrex_guest_program::l2::run_guest; +#[cfg(not(feature = "l2"))] +use ethrex_guest_program::l1::run_stateless_guest; use ethrex_guest_program::crypto::risc0::Risc0Crypto; use risc0_zkvm::guest::env; @@ -16,27 +14,21 @@ fn main() { let start = env::cycle_count(); let mut input = Vec::new(); env::stdin().read_to_end(&mut input).unwrap(); - - #[cfg(not(feature = "eip-8025"))] - let input = { - use rkyv::rancor::Error; - rkyv::from_bytes::(&input).unwrap() - }; let end = env::cycle_count(); println!("end reading input, cycles: {}", end - start); let crypto = Arc::new(Risc0Crypto); println!("start execution"); - #[cfg(feature = "eip-8025")] - let output = execution_program(&input, crypto).unwrap(); - #[cfg(not(feature = "eip-8025"))] - let output = execution_program(input, crypto).unwrap(); + #[cfg(not(feature = "l2"))] + let output = run_stateless_guest(&input, crypto); + #[cfg(feature = "l2")] + let output = run_guest(&input, crypto).unwrap(); let end_exec = env::cycle_count(); println!("end execution, cycles: {}", end_exec - end); println!("start committing public inputs"); - env::commit_slice(&output.encode()); + env::commit_slice(&output); let end_commit = env::cycle_count(); println!( "end committing public inputs, cycles: {}", diff --git a/crates/guest-program/bin/sp1/Cargo.toml b/crates/guest-program/bin/sp1/Cargo.toml index b47fcd977b0..355d4d81887 100644 --- a/crates/guest-program/bin/sp1/Cargo.toml +++ b/crates/guest-program/bin/sp1/Cargo.toml @@ -36,4 +36,3 @@ bls12_381 = { git = "https://github.com/lambdaclass/bls12_381-patch/", branch = [features] l2 = ["ethrex-guest-program/l2", "sp1-zkvm/embedded"] -eip-8025 = ["ethrex-guest-program/eip-8025"] diff --git a/crates/guest-program/bin/sp1/src/main.rs b/crates/guest-program/bin/sp1/src/main.rs index 3046bd3a842..8b5d30cb4e8 100644 --- a/crates/guest-program/bin/sp1/src/main.rs +++ b/crates/guest-program/bin/sp1/src/main.rs @@ -3,36 +3,29 @@ use std::sync::Arc; #[cfg(feature = "l2")] -use ethrex_guest_program::l2::{ProgramInput, execution_program}; -#[cfg(all(not(feature = "l2"), not(feature = "eip-8025")))] -use ethrex_guest_program::l1::{ProgramInput, execution_program}; -#[cfg(all(not(feature = "l2"), feature = "eip-8025"))] -use ethrex_guest_program::l1::execution_program; +use ethrex_guest_program::l2::run_guest; +#[cfg(not(feature = "l2"))] +use ethrex_guest_program::l1::run_stateless_guest; use ethrex_guest_program::crypto::sp1::Sp1Crypto; -#[cfg(not(feature = "eip-8025"))] -use rkyv::rancor::Error; sp1_zkvm::entrypoint!(main); pub fn main() { println!("cycle-tracker-report-start: read_input"); let input = sp1_zkvm::io::read_vec(); - - #[cfg(not(feature = "eip-8025"))] - let input = { rkyv::from_bytes::(&input).unwrap() }; println!("cycle-tracker-report-end: read_input"); let crypto = Arc::new(Sp1Crypto); println!("cycle-tracker-report-start: execution"); - #[cfg(feature = "eip-8025")] - let output = execution_program(&input, crypto).unwrap(); - #[cfg(not(feature = "eip-8025"))] - let output = execution_program(input, crypto).unwrap(); + #[cfg(not(feature = "l2"))] + let output = run_stateless_guest(&input, crypto); + #[cfg(feature = "l2")] + let output = run_guest(&input, crypto).unwrap(); println!("cycle-tracker-report-end: execution"); println!("cycle-tracker-report-start: commit_public_inputs"); - sp1_zkvm::io::commit_slice(&output.encode()); + sp1_zkvm::io::commit_slice(&output); println!("cycle-tracker-report-end: commit_public_inputs"); } diff --git a/crates/guest-program/bin/zisk/Cargo.toml b/crates/guest-program/bin/zisk/Cargo.toml index 28b748cdc2c..812bf5b6e91 100644 --- a/crates/guest-program/bin/zisk/Cargo.toml +++ b/crates/guest-program/bin/zisk/Cargo.toml @@ -29,4 +29,3 @@ ethrex-common = { path = "../../../common", default-features = false, features = [features] l2 = ["ethrex-guest-program/l2"] -eip-8025 = ["ethrex-guest-program/eip-8025"] diff --git a/crates/guest-program/bin/zisk/src/main.rs b/crates/guest-program/bin/zisk/src/main.rs index 41e5c15ddf5..ce2bcaecaf5 100644 --- a/crates/guest-program/bin/zisk/src/main.rs +++ b/crates/guest-program/bin/zisk/src/main.rs @@ -3,36 +3,29 @@ use std::sync::Arc; #[cfg(feature = "l2")] -use ethrex_guest_program::l2::{ProgramInput, execution_program}; -#[cfg(all(not(feature = "l2"), not(feature = "eip-8025")))] -use ethrex_guest_program::l1::{ProgramInput, execution_program}; -#[cfg(all(not(feature = "l2"), feature = "eip-8025"))] -use ethrex_guest_program::l1::execution_program; +use ethrex_guest_program::l2::run_guest; +#[cfg(not(feature = "l2"))] +use ethrex_guest_program::l1::run_stateless_guest; use ethrex_guest_program::crypto::zisk::ZiskCrypto; -#[cfg(not(feature = "eip-8025"))] -use rkyv::rancor::Error; ziskos::entrypoint!(main); pub fn main() { println!("start reading input"); let input = ziskos::io::read_slice(); - - #[cfg(not(feature = "eip-8025"))] - let input = { rkyv::from_bytes::(&input).unwrap() }; println!("finish reading input"); let crypto = Arc::new(ZiskCrypto); println!("start execution"); - #[cfg(feature = "eip-8025")] - let output = execution_program(&input, crypto).unwrap(); - #[cfg(not(feature = "eip-8025"))] - let output = execution_program(input, crypto).unwrap(); + #[cfg(not(feature = "l2"))] + let output = run_stateless_guest(&input, crypto); + #[cfg(feature = "l2")] + let output = run_guest(&input, crypto).unwrap(); println!("finish execution"); println!("start revealing output"); - ziskos::io::commit_slice(&output.encode()); + ziskos::io::commit_slice(&output); println!("finish revealing output"); } diff --git a/crates/guest-program/src/l1/input.rs b/crates/guest-program/src/l1/input.rs index 3dc1f238170..3c99c486640 100644 --- a/crates/guest-program/src/l1/input.rs +++ b/crates/guest-program/src/l1/input.rs @@ -1,388 +1,63 @@ -//! `ProgramInput` is a `struct` without the `eip-8025` feature and an `enum` -//! with it. The `new(...)` constructor and `Default` exist under both, but -//! pattern-matching on `Wire(...)`/`Direct { .. }` only compiles when -//! `eip-8025` is on. - -use ethrex_common::types::Block; -use ethrex_common::types::block_execution_witness::ExecutionWitness; - -/// Input for the L1 stateless validation program. -#[cfg(not(feature = "eip-8025"))] -#[derive( - Clone, - Default, - serde::Serialize, - serde::Deserialize, - rkyv::Deserialize, - rkyv::Serialize, - rkyv::Archive, -)] -pub struct ProgramInput { - /// Blocks to execute. - pub blocks: Vec, - /// Database containing all the data necessary to execute. - pub execution_witness: ExecutionWitness, -} - -#[cfg(not(feature = "eip-8025"))] -impl ProgramInput { - /// Creates a new ProgramInput with the given blocks and execution witness. - pub fn new(blocks: Vec, execution_witness: ExecutionWitness) -> Self { - Self { - blocks, - execution_witness, - } - } -} - -/// Input for the L1 stateless validation program (EIP-8025 build). -/// -/// `Direct` carries in-memory blocks + witness (test path). `Wire` carries an -/// already-decoded EIP-8025 stateless input from spec wire bytes. -#[cfg(feature = "eip-8025")] -pub enum ProgramInput { - Direct { - blocks: Vec, - execution_witness: ExecutionWitness, - }, - Wire(DecodedEip8025), -} - -#[cfg(feature = "eip-8025")] -impl Default for ProgramInput { - fn default() -> Self { - Self::Direct { - blocks: Vec::new(), - execution_witness: ExecutionWitness::default(), - } - } -} - -#[cfg(feature = "eip-8025")] -impl ProgramInput { - /// Creates a `Direct` ProgramInput from in-memory blocks and execution witness. - pub fn new(blocks: Vec, execution_witness: ExecutionWitness) -> Self { - Self::Direct { - blocks, - execution_witness, - } - } - - /// Creates a `Wire` ProgramInput from an already-decoded EIP-8025 payload. - pub fn wire(decoded: DecodedEip8025) -> Self { - Self::Wire(decoded) - } -} - -/// Wire-format version byte for the legacy EIP-8025 framing. -#[cfg(feature = "eip-8025")] -pub const EIP8025_VERSION_LEGACY: u8 = 0x00; - -/// Wire-format version byte for the canonical EIP-8025 framing. -#[cfg(feature = "eip-8025")] -pub const EIP8025_VERSION_CANONICAL: u8 = 0x01; - -/// Encode a `NewPayloadRequest` (SSZ) and `ExecutionWitness` (rkyv) into the -/// legacy EIP-8025 length-prefixed wire format: +//! Decoding of `statelessInputBytes`, the L1 guest's only input. +//! +//! The wire format is a 2-byte big-endian schema id followed by the SSZ-encoded +//! `SszStatelessInput`, matching `deserialize_stateless_input` in +//! `stateless_guest.py` at execution-specs `3c3b6f4af` (#3248 + #3278). +//! +//! The containers themselves live in `ethrex_common::types::stateless_ssz`, +//! shared with the EXECUTE precompile path — this module is only the framing. +//! Amsterdam is the sole schema the spec defines, so there is no fork dispatch: +//! the id fully determines both the fork rules and the encoding. + +use ethrex_common::types::stateless_ssz::{ + STATELESS_INPUT_SCHEMA_ID, STATELESS_INPUT_SCHEMA_ID_SIZE, SszStatelessInput, +}; + +/// Decode schema-prefixed `statelessInputBytes`. /// -/// `[version=0x00] [ssz_len: u32 LE] [ssz_bytes] [rkyv_bytes]` -/// -/// Returns an error if rkyv serialization of the execution witness fails. -#[cfg(feature = "eip-8025")] -pub fn encode_eip8025( - new_payload_request: ðrex_common::types::stateless_ssz::NewPayloadRequest, - execution_witness: &ExecutionWitness, -) -> Result, ProgramInputEncodeError> { - use libssz::SszEncode; - - let ssz_bytes = new_payload_request.to_ssz(); - let ssz_len = ssz_bytes.len() as u32; - let rkyv_bytes = rkyv::to_bytes::(execution_witness) - .map_err(|e| ProgramInputEncodeError::Rkyv(e.to_string()))?; - - let mut out = Vec::with_capacity(1 + 4 + ssz_bytes.len() + rkyv_bytes.len()); - out.push(EIP8025_VERSION_LEGACY); - out.extend_from_slice(&ssz_len.to_le_bytes()); - out.extend_from_slice(&ssz_bytes); - out.extend_from_slice(&rkyv_bytes); - Ok(out) -} - -// ── canonical SSZ schema ─────────────────────────────────────────── - -#[cfg(feature = "eip-8025")] -const MAX_WITNESS_NODES: usize = 1 << 22; -#[cfg(feature = "eip-8025")] -const MAX_WITNESS_CODES: usize = 1 << 18; -#[cfg(feature = "eip-8025")] -const MAX_WITNESS_HEADERS: usize = 256; -#[cfg(feature = "eip-8025")] -const MAX_BYTES_PER_WITNESS_NODE: usize = 1 << 10; -#[cfg(feature = "eip-8025")] -const MAX_BYTES_PER_CODE: usize = 1 << 16; -#[cfg(feature = "eip-8025")] -const MAX_BYTES_PER_HEADER: usize = 1 << 10; -#[cfg(feature = "eip-8025")] -const MAX_PUBLIC_KEYS: usize = 1 << 15; -#[cfg(feature = "eip-8025")] -const BYTES_PER_PUBLIC_KEY: usize = 65; - -/// SSZ shape of the per-tx public key list in `CanonicalStatelessInput`: -/// one fixed-size 65-byte uncompressed secp256k1 key per transaction. -#[cfg(feature = "eip-8025")] -pub type PublicKeysList = - libssz_types::SszList, MAX_PUBLIC_KEYS>; -#[cfg(feature = "eip-8025")] -const MAX_OPTIONAL_FORK_ACTIVATION_VALUES: usize = 1; - -/// Big-endian schema-id prefix on canonical `SszStatelessInput` wire bytes. -/// Per EELS `stateless_ssz.py`, `schema_id = (fork_index << 8) | revision`. -/// Amsterdam is fork `0x15`, revision `0x01` (SSZ `SszStatelessInput` payload). -#[cfg(feature = "eip-8025")] -pub const STATELESS_INPUT_SCHEMA_ID: u16 = 0x1501; -/// Byte length of [`STATELESS_INPUT_SCHEMA_ID`] on the wire. -#[cfg(feature = "eip-8025")] -pub const STATELESS_INPUT_SCHEMA_ID_SIZE: usize = 2; - -/// Mirrors `SszForkActivation` from the Amsterdam stateless-validation spec. -#[cfg(feature = "eip-8025")] -#[derive(Debug, Clone, PartialEq, Eq, libssz_derive::SszEncode, libssz_derive::SszDecode)] -pub struct CanonicalForkActivation { - pub block_number: libssz_types::SszList, - pub timestamp: libssz_types::SszList, -} - -/// Mirrors `SszForkConfig` from the Amsterdam stateless-validation spec. -/// As of glamsterdam-devnet-7 this carries only `activation`; the earlier -/// `fork` id and per-fork `blob_schedule` fields were dropped upstream. -#[cfg(feature = "eip-8025")] -#[derive(Debug, Clone, PartialEq, Eq, libssz_derive::SszEncode, libssz_derive::SszDecode)] -pub struct CanonicalForkConfig { - pub activation: CanonicalForkActivation, -} - -/// Mirrors `SszChainConfig` from the Amsterdam stateless-validation spec. -#[cfg(feature = "eip-8025")] -#[derive(Debug, Clone, PartialEq, Eq, libssz_derive::SszEncode, libssz_derive::SszDecode)] -pub struct CanonicalChainConfig { - pub chain_id: u64, - pub active_fork: CanonicalForkConfig, -} - -/// Mirrors `SszExecutionWitness` from the Amsterdam stateless-validation spec. -#[cfg(feature = "eip-8025")] -#[derive(Debug, Clone, PartialEq, Eq, libssz_derive::SszEncode, libssz_derive::SszDecode)] -pub struct CanonicalExecutionWitness { - pub state: libssz_types::SszList< - libssz_types::SszList, - MAX_WITNESS_NODES, - >, - pub codes: - libssz_types::SszList, MAX_WITNESS_CODES>, - pub headers: - libssz_types::SszList, MAX_WITNESS_HEADERS>, -} - -/// Mirrors `SszStatelessInput` from the Amsterdam stateless-validation spec. -#[cfg(feature = "eip-8025")] -#[derive(Debug, Clone, PartialEq, Eq, libssz_derive::SszEncode, libssz_derive::SszDecode)] -pub struct CanonicalStatelessInput { - pub new_payload_request: ethrex_common::types::eip8025_ssz::NewPayloadRequestAmsterdam, - pub witness: CanonicalExecutionWitness, - pub chain_config: CanonicalChainConfig, - /// Per-transaction public keys (uncompressed secp256k1, 65 bytes each). - /// Mirrors `SszList[ByteVector[PUBLIC_KEY_BYTES], MAX_PUBLIC_KEYS]` in the spec. - pub public_keys: PublicKeysList, -} - -/// Decoded EIP-8025 wire payload, dispatched by version byte. -#[cfg(feature = "eip-8025")] -pub enum DecodedEip8025 { - /// Legacy framing (`version = 0x00`). - Legacy { - new_payload_request: ethrex_common::types::eip8025_ssz::NewPayloadRequest, - execution_witness: ExecutionWitness, - }, - /// Canonical-input framing (`version = 0x01`). - Canonical { - stateless_input: CanonicalStatelessInput, - chain_config: ethrex_common::types::ChainConfig, - }, -} - -#[cfg(feature = "eip-8025")] -impl core::fmt::Debug for DecodedEip8025 { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - DecodedEip8025::Legacy { .. } => f.write_str("DecodedEip8025::Legacy"), - DecodedEip8025::Canonical { .. } => f.write_str("DecodedEip8025::Canonical"), - } - } -} - -/// Decode an EIP-8025 wire blob. -/// -/// The first byte is a version discriminator: -/// - `0x00` → legacy framing -/// (`[ssz_len: u32 LE] [ssz_bytes] [rkyv ExecutionWitness]`). -/// - `0x01` → canonical-input framing -/// (`[ssz_len: u32 LE] [ssz_bytes] [cfg_len: u32 LE] [rkyv ChainConfig]`). -/// -/// Anything else surfaces as [`ProgramInputDecodeError::UnknownVersion`]. -#[cfg(feature = "eip-8025")] -pub fn decode_eip8025(bytes: &[u8]) -> Result { - let (version, rest) = bytes - .split_first() - .ok_or(ProgramInputDecodeError::TooShort)?; - match *version { - EIP8025_VERSION_LEGACY => { - let (new_payload_request, execution_witness) = decode_eip8025_legacy(rest)?; - Ok(DecodedEip8025::Legacy { - new_payload_request, - execution_witness, - }) - } - EIP8025_VERSION_CANONICAL => { - let (stateless_input, chain_config) = decode_eip8025_canonical(rest)?; - Ok(DecodedEip8025::Canonical { - stateless_input, - chain_config, - }) - } - v => Err(ProgramInputDecodeError::UnknownVersion(v)), - } -} - -/// Decode a spec-format canonical stateless input blob: -/// `[BE u16 STATELESS_INPUT_SCHEMA_ID][SSZ-encoded CanonicalStatelessInput]`. -/// Caller supplies `chain_config` out-of-band (unlike [`decode_eip8025`]). -#[cfg(feature = "eip-8025")] -pub fn decode_canonical_stateless_input_bytes( +/// Rejects any schema id other than [`STATELESS_INPUT_SCHEMA_ID`]. That check is +/// load-bearing rather than defensive: since #3278 no chain configuration crosses +/// the wire, so the prefix is the only thing identifying which fork's rules the +/// guest should apply. Upstream rejects it the same way. +pub fn decode_stateless_input( bytes: &[u8], -) -> Result { +) -> Result { use libssz::SszDecode; - if bytes.len() < STATELESS_INPUT_SCHEMA_ID_SIZE { - return Err(ProgramInputDecodeError::TooShort); - } - let (schema_bytes, ssz_bytes) = bytes.split_at(STATELESS_INPUT_SCHEMA_ID_SIZE); - let schema_id = u16::from_be_bytes([schema_bytes[0], schema_bytes[1]]); + let (id_bytes, body) = bytes + .split_first_chunk::() + .ok_or(StatelessInputDecodeError::MissingSchemaId)?; + let schema_id = u16::from_be_bytes(*id_bytes); if schema_id != STATELESS_INPUT_SCHEMA_ID { - return Err(ProgramInputDecodeError::UnknownSchemaId(schema_id)); - } - CanonicalStatelessInput::from_ssz_bytes(ssz_bytes).map_err(ProgramInputDecodeError::Ssz) -} - -#[cfg(feature = "eip-8025")] -fn decode_eip8025_legacy( - bytes: &[u8], -) -> Result< - ( - ethrex_common::types::eip8025_ssz::NewPayloadRequest, - ExecutionWitness, - ), - ProgramInputDecodeError, -> { - use libssz::SszDecode; - - if bytes.len() < 4 { - return Err(ProgramInputDecodeError::TooShort); - } - // Safety: we already checked bytes.len() >= 4 above, so this slice is exactly 4 bytes. - let ssz_len = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize; - if bytes.len() < 4 + ssz_len { - return Err(ProgramInputDecodeError::TooShort); - } - let ssz_bytes = &bytes[4..4 + ssz_len]; - let rkyv_bytes = &bytes[4 + ssz_len..]; - - let new_payload_request = - ethrex_common::types::eip8025_ssz::NewPayloadRequest::from_ssz_bytes(ssz_bytes) - .map_err(ProgramInputDecodeError::Ssz)?; - let execution_witness = rkyv::from_bytes::(rkyv_bytes) - .map_err(|e| ProgramInputDecodeError::Rkyv(e.to_string()))?; - - Ok((new_payload_request, execution_witness)) -} - -#[cfg(feature = "eip-8025")] -fn decode_eip8025_canonical( - bytes: &[u8], -) -> Result<(CanonicalStatelessInput, ethrex_common::types::ChainConfig), ProgramInputDecodeError> { - use libssz::SszDecode; - - if bytes.len() < 4 { - return Err(ProgramInputDecodeError::TooShort); + return Err(StatelessInputDecodeError::UnsupportedSchemaId(schema_id)); } - let ssz_len = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize; - let cfg_len_off = 4usize - .checked_add(ssz_len) - .ok_or(ProgramInputDecodeError::TooShort)?; - if bytes.len() < cfg_len_off + 4 { - return Err(ProgramInputDecodeError::TooShort); - } - let ssz_bytes = &bytes[4..cfg_len_off]; - - let cfg_len = u32::from_le_bytes([ - bytes[cfg_len_off], - bytes[cfg_len_off + 1], - bytes[cfg_len_off + 2], - bytes[cfg_len_off + 3], - ]) as usize; - let cfg_off = cfg_len_off + 4; - let cfg_end = cfg_off - .checked_add(cfg_len) - .ok_or(ProgramInputDecodeError::TooShort)?; - if bytes.len() < cfg_end { - return Err(ProgramInputDecodeError::TooShort); - } - let cfg_bytes = &bytes[cfg_off..cfg_end]; - - let stateless_input = - CanonicalStatelessInput::from_ssz_bytes(ssz_bytes).map_err(ProgramInputDecodeError::Ssz)?; - let chain_config = - rkyv::from_bytes::(cfg_bytes) - .map_err(|e| ProgramInputDecodeError::Rkyv(e.to_string()))?; - - Ok((stateless_input, chain_config)) -} -#[cfg(feature = "eip-8025")] -#[derive(Debug)] -pub enum ProgramInputEncodeError { - Rkyv(String), + SszStatelessInput::from_ssz_bytes(body).map_err(StatelessInputDecodeError::Ssz) } -#[cfg(feature = "eip-8025")] -impl core::fmt::Display for ProgramInputEncodeError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::Rkyv(e) => write!(f, "rkyv encode error: {e}"), - } - } -} - -#[cfg(feature = "eip-8025")] +/// Why a `statelessInputBytes` blob could not be decoded. +/// +/// Every variant is a decode failure, which the guest commits as the all-zero +/// default result rather than surfacing as an error — see +/// [`super::run_stateless_guest`]. #[derive(Debug)] -pub enum ProgramInputDecodeError { - TooShort, +pub enum StatelessInputDecodeError { + /// Fewer than two bytes, so there is no schema id to read. + MissingSchemaId, + /// A well-formed id that is not the one schema this guest implements. + UnsupportedSchemaId(u16), + /// The id matched but the SSZ body did not decode. Ssz(libssz::DecodeError), - Rkyv(String), - UnknownVersion(u8), - UnknownSchemaId(u16), } -#[cfg(feature = "eip-8025")] -impl core::fmt::Display for ProgramInputDecodeError { +impl core::fmt::Display for StatelessInputDecodeError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { - Self::TooShort => write!(f, "input too short"), - Self::Ssz(e) => write!(f, "SSZ decode error: {e}"), - Self::Rkyv(e) => write!(f, "rkyv decode error: {e}"), - Self::UnknownVersion(v) => write!(f, "unknown EIP-8025 wire version: {v:#04x}"), - Self::UnknownSchemaId(v) => { - write!(f, "unknown stateless input schema id: {v:#06x}") + Self::MissingSchemaId => write!(f, "input too short to contain a schema id"), + Self::UnsupportedSchemaId(id) => { + write!(f, "unsupported stateless input schema id: {id:#06x}") } + Self::Ssz(e) => write!(f, "SSZ decode error: {e}"), } } } diff --git a/crates/guest-program/src/l1/mod.rs b/crates/guest-program/src/l1/mod.rs index 0f2010c960a..276c9d8cc52 100644 --- a/crates/guest-program/src/l1/mod.rs +++ b/crates/guest-program/src/l1/mod.rs @@ -1,22 +1,8 @@ mod input; -mod output; mod program; -pub use input::ProgramInput; -#[cfg(feature = "eip-8025")] -pub use input::{ - CanonicalChainConfig, CanonicalExecutionWitness, CanonicalForkActivation, CanonicalForkConfig, - CanonicalStatelessInput, DecodedEip8025, EIP8025_VERSION_CANONICAL, EIP8025_VERSION_LEGACY, - decode_canonical_stateless_input_bytes, decode_eip8025, encode_eip8025, -}; -#[cfg(feature = "eip-8025")] -pub use input::{ProgramInputDecodeError, ProgramInputEncodeError}; -pub use output::ProgramOutput; -pub use program::execution_program; -pub use program::new_payload_request_to_block; -pub use program::validate_public_keys; -pub use program::verify_stateless_block; -#[cfg(feature = "eip-8025")] +pub use input::{StatelessInputDecodeError, decode_stateless_input}; pub use program::{ - execute_decoded, validate_eip8025_canonical_execution, validate_eip8025_execution, + new_payload_request_to_block, run_stateless_guest, validate_blocks_statelessly, + validate_public_keys, validate_stateless_execution, verify_stateless_block, }; diff --git a/crates/guest-program/src/l1/output.rs b/crates/guest-program/src/l1/output.rs deleted file mode 100644 index f22126780c2..00000000000 --- a/crates/guest-program/src/l1/output.rs +++ /dev/null @@ -1,63 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[cfg(not(feature = "eip-8025"))] -use ethrex_common::{H256, U256}; - -/// Output of the L1 stateless validation program. -#[cfg(not(feature = "eip-8025"))] -#[derive(Serialize, Deserialize)] -pub struct ProgramOutput { - /// Initial state trie root hash. - pub initial_state_hash: H256, - /// Final state trie root hash. - pub final_state_hash: H256, - /// Hash of the last block in the batch. - pub last_block_hash: H256, - /// Chain ID of the network. - pub chain_id: U256, - /// Number of transactions in the batch. - pub transaction_count: U256, -} - -#[cfg(not(feature = "eip-8025"))] -impl ProgramOutput { - /// Encode the output to bytes for commitment. - pub fn encode(&self) -> Vec { - [ - self.initial_state_hash.to_fixed_bytes(), - self.final_state_hash.to_fixed_bytes(), - self.last_block_hash.to_fixed_bytes(), - self.chain_id.to_big_endian(), - self.transaction_count.to_big_endian(), - ] - .concat() - } -} - -/// Output of the L1 stateless validation program (EIP-8025). -/// -/// The output is a 41-byte commitment: the `hash_tree_root` of the -/// `NewPayloadRequest` (32 bytes), a validity flag (1 byte), and -/// `chain_id` (8 bytes). -#[cfg(feature = "eip-8025")] -#[derive(Serialize, Deserialize)] -pub struct ProgramOutput { - /// The `hash_tree_root` of the `NewPayloadRequest`. - pub new_payload_request_root: [u8; 32], - /// Whether execution was valid. - pub valid: bool, - /// Chain ID from the stateless validation chain configuration. - pub chain_id: u64, -} - -#[cfg(feature = "eip-8025")] -impl ProgramOutput { - /// Encode the output to 41 bytes: `root ++ valid ++ chain_id`. - pub fn encode(&self) -> Vec { - let mut out = Vec::with_capacity(41); - out.extend_from_slice(&self.new_payload_request_root); - out.push(u8::from(self.valid)); - out.extend_from_slice(&self.chain_id.to_le_bytes()); - out - } -} diff --git a/crates/guest-program/src/l1/program.rs b/crates/guest-program/src/l1/program.rs index 13c03cf2cea..3fdf93b0a3d 100644 --- a/crates/guest-program/src/l1/program.rs +++ b/crates/guest-program/src/l1/program.rs @@ -1,224 +1,29 @@ use std::sync::Arc; -#[cfg(feature = "eip-8025")] -use ethrex_common::Address; -#[cfg(feature = "eip-8025")] -use ethrex_common::utils::keccak; -use ethrex_crypto::Crypto; - -use crate::common::ExecutionError; -use crate::common::execute_blocks; -use crate::l1::input::ProgramInput; -#[cfg(feature = "eip-8025")] -use crate::l1::input::{ - CanonicalExecutionWitness, CanonicalStatelessInput, DecodedEip8025, PublicKeysList, -}; -use crate::l1::output::ProgramOutput; - use ethrex_common::types::ELASTICITY_MULTIPLIER; +use ethrex_common::types::stateless_ssz::{ + STATELESS_INPUT_SCHEMA_ID, SszPublicKeys, SszStatelessInput, SszStatelessValidationResult, +}; use ethrex_common::validate_block_access_list_hash; +use ethrex_crypto::Crypto; use ethrex_vm::Evm; +use libssz_merkle::{HashTreeRoot, Sha256Hasher}; -#[cfg(feature = "eip-8025")] -use libssz_merkle::Sha256Hasher; - -#[cfg(not(feature = "eip-8025"))] -use crate::common::BatchExecutionResult; - -/// Execute the L1 stateless validation program. -/// -/// This validates and executes a batch of L1 blocks, verifying state transitions -/// without access to the full blockchain state. -#[cfg(not(feature = "eip-8025"))] -pub fn execution_program( - input: ProgramInput, - crypto: Arc, -) -> Result { - let ProgramInput { - blocks, - execution_witness, - } = input; - - let BatchExecutionResult { - receipts: _, - initial_state_hash, - final_state_hash, - last_block_hash, - non_privileged_count, - chain_id, - burned_fees: _, - bals: _, - } = execute_blocks( - &blocks, - execution_witness, - ELASTICITY_MULTIPLIER, - |db, _| { - // L1 VM factory - simple creation without fee configs - Ok(Evm::new_for_l1(db.clone(), crypto.clone())) - }, - crypto.clone(), - )?; - - Ok(ProgramOutput { - initial_state_hash, - final_state_hash, - last_block_hash, - chain_id: chain_id.into(), - transaction_count: non_privileged_count, - }) -} +use crate::common::ExecutionError; +use crate::common::execute_blocks; +use crate::l1::input::decode_stateless_input; /// Wrapper to bridge `ethrex_crypto::Crypto` to `libssz_merkle::Sha256Hasher`, /// so `hash_tree_root` is computed via crypto precompiles in the zkVM. /// Required because the orphan rule prevents a direct impl on `Arc`. -#[cfg(feature = "eip-8025")] struct CryptoWrapper(Arc); -#[cfg(feature = "eip-8025")] impl Sha256Hasher for CryptoWrapper { fn hash(&self, data: &[u8]) -> [u8; 32] { self.0.sha256(data) } } -/// Decode and execute the L1 stateless validation program from EIP-8025 wire -/// bytes. -/// -/// The wire format is version-prefixed; see [`super::decode_eip8025`] for the -/// per-version layout. Legacy and canonical-input payloads both commit to the -/// decoded `NewPayloadRequest` root and report execution validity as a boolean. -#[cfg(feature = "eip-8025")] -pub fn execution_program( - bytes: &[u8], - crypto: Arc, -) -> Result { - let decoded = super::decode_eip8025(bytes).map_err(|err| { - ExecutionError::Internal(format!("failed to decode EIP-8025 input: {err}")) - })?; - - execute_decoded(ProgramInput::Wire(decoded), crypto) -} - -/// Execute an already-built [`ProgramInput`]. -/// -/// The `Direct` arm has no `NewPayloadRequest`, so it returns a sentinel -/// `ProgramOutput` with zero request_root and `valid = true`. `ExecBackend` -/// promotes `valid = false` to `Err` for result-only callers. -#[cfg(feature = "eip-8025")] -pub fn execute_decoded( - input: ProgramInput, - crypto: Arc, -) -> Result { - use libssz_merkle::HashTreeRoot; - - match input { - ProgramInput::Direct { - blocks, - execution_witness, - } => { - let chain_id = execution_witness.chain_config.chain_id; - execute_blocks( - &blocks, - execution_witness, - ELASTICITY_MULTIPLIER, - |db, _| Ok(Evm::new_for_l1(db.clone(), crypto.clone())), - crypto.clone(), - )?; - Ok(ProgramOutput { - new_payload_request_root: [0u8; 32], - valid: true, - chain_id, - }) - } - ProgramInput::Wire(DecodedEip8025::Legacy { - new_payload_request, - execution_witness, - }) => { - let request_root = new_payload_request.hash_tree_root(&CryptoWrapper(crypto.clone())); - let chain_id = execution_witness.chain_config.chain_id; - let valid = - validate_eip8025_execution(&new_payload_request, execution_witness, crypto).is_ok(); - - Ok(ProgramOutput { - new_payload_request_root: request_root, - valid, - chain_id, - }) - } - ProgramInput::Wire(DecodedEip8025::Canonical { - stateless_input, - chain_config, - }) => Ok(execute_canonical_stateless_input_decoded( - stateless_input, - chain_config, - crypto, - )), - } -} - -#[cfg(feature = "eip-8025")] -fn execute_canonical_stateless_input_decoded( - stateless_input: CanonicalStatelessInput, - chain_config: ethrex_common::types::ChainConfig, - crypto: Arc, -) -> ProgramOutput { - use libssz_merkle::HashTreeRoot; - - let request_root = stateless_input - .new_payload_request - .hash_tree_root(&CryptoWrapper(crypto.clone())); - let chain_id = stateless_input.chain_config.chain_id; - let valid = validate_eip8025_canonical_execution(stateless_input, chain_config, crypto).is_ok(); - - ProgramOutput { - new_payload_request_root: request_root, - valid, - chain_id, - } -} - -#[cfg(feature = "eip-8025")] -fn decode_payload_transactions( - transactions: &libssz_types::SszList, MAX_TXS>, -) -> Result, String> { - transactions - .iter() - .map(|tx_bytes| { - ethrex_common::types::Transaction::decode_canonical(tx_bytes) - .map_err(|e| format!("tx decode: {e}")) - }) - .collect::, _>>() -} - -#[cfg(feature = "eip-8025")] -fn decode_payload_withdrawals( - withdrawals: &libssz_types::SszList< - ethrex_common::types::eip8025_ssz::Withdrawal, - MAX_WITHDRAWALS, - >, -) -> Vec { - use ethrex_common::Address; - - withdrawals - .iter() - .map(|w| ethrex_common::types::Withdrawal { - index: w.index, - validator_index: w.validator_index, - address: Address::from_slice(&w.address.0), - amount: w.amount, - }) - .collect() -} - -/// Convert a 32-byte little-endian SSZ `uint256` base-fee field to `u64`. -/// -/// The upper 24 bytes (`[8..32]`) MUST be zero. They don't affect block validation -/// (base fee fits in `u64` for any real chain), but they ARE covered by -/// `NewPayloadRequest::hash_tree_root()`. Silently truncating to the low 8 bytes -/// would let ~2^192 distinct SSZ inputs reconstruct the *same* block while producing -/// *different* roots, breaking the "one block ⇒ one root" commitment invariant for -/// any root-keyed consumer (e.g. a ZK variant or a root-anchored settlement path). -/// Rejecting non-zero upper bytes closes that malleability. fn base_fee_per_gas_from_le_bytes(bytes: &[u8; 32]) -> Result { if bytes[8..].iter().any(|&b| b != 0) { return Err("base_fee_per_gas exceeds u64 (non-zero upper bytes)".to_string()); @@ -230,163 +35,7 @@ fn base_fee_per_gas_from_le_bytes(bytes: &[u8; 32]) -> Result { )) } -#[cfg(feature = "eip-8025")] -fn validate_reconstructed_block_hash( - block: ðrex_common::types::Block, - expected_hash: &[u8; 32], - crypto: &dyn Crypto, -) -> Result<(), String> { - let computed_hash = block.header.compute_block_hash(crypto); - let expected_hash = ethrex_common::H256::from_slice(expected_hash); - if computed_hash != expected_hash { - return Err(format!( - "block_hash mismatch: expected {expected_hash:?}, got {computed_hash:?}" - )); - } - - Ok(()) -} - /// Transform an SSZ `NewPayloadRequest` into a `Block`. -#[cfg(feature = "eip-8025")] -fn eip8025_new_payload_request_to_block( - req: ðrex_common::types::eip8025_ssz::NewPayloadRequest, - crypto: &dyn Crypto, -) -> Result { - use bytes::Bytes; - use ethrex_common::constants::DEFAULT_OMMERS_HASH; - use ethrex_common::types::requests::compute_requests_hash; - use ethrex_common::types::{ - Block, BlockBody, BlockHeader, compute_transactions_root, compute_withdrawals_root, - }; - use ethrex_common::{Address, Bloom, H256}; - - let payload = &req.execution_payload; - - let transactions = decode_payload_transactions(&payload.transactions)?; - - let withdrawals = decode_payload_withdrawals(&payload.withdrawals); - - // Build execution_requests from the SSZ typed ExecutionRequests field - let execution_requests = req.execution_requests.to_encoded_requests(); - let requests_hash = compute_requests_hash(&execution_requests); - - let base_fee_per_gas = base_fee_per_gas_from_le_bytes(&payload.base_fee_per_gas)?; - let logs_bloom = Bloom::from_slice(&payload.logs_bloom); - - let transactions_root = compute_transactions_root(&transactions, crypto); - let withdrawals_root = compute_withdrawals_root(&withdrawals, crypto); - - let body = BlockBody { - transactions, - ommers: vec![], - withdrawals: Some(withdrawals), - }; - - let header = BlockHeader { - parent_hash: H256::from_slice(&payload.parent_hash), - ommers_hash: *DEFAULT_OMMERS_HASH, - coinbase: Address::from_slice(&payload.fee_recipient.0), - state_root: H256::from_slice(&payload.state_root), - transactions_root, - receipts_root: H256::from_slice(&payload.receipts_root), - logs_bloom, - difficulty: 0.into(), - number: payload.block_number, - gas_limit: payload.gas_limit, - gas_used: payload.gas_used, - timestamp: payload.timestamp, - extra_data: Bytes::copy_from_slice(&payload.extra_data), - prev_randao: H256::from_slice(&payload.prev_randao), - nonce: 0, - base_fee_per_gas: Some(base_fee_per_gas), - withdrawals_root: Some(withdrawals_root), - blob_gas_used: Some(payload.blob_gas_used), - excess_blob_gas: Some(payload.excess_blob_gas), - parent_beacon_block_root: Some(H256::from_slice(&req.parent_beacon_block_root)), - requests_hash: Some(requests_hash), - ..Default::default() - }; - - Ok(Block::new(header, body)) -} - -/// Transform an Amsterdam SSZ `NewPayloadRequest` into a `Block`. -#[cfg(feature = "eip-8025")] -fn new_payload_request_amsterdam_to_block( - req: ðrex_common::types::eip8025_ssz::NewPayloadRequestAmsterdam, - crypto: &dyn Crypto, -) -> Result { - use bytes::Bytes; - use ethrex_common::constants::DEFAULT_OMMERS_HASH; - use ethrex_common::types::block_access_list::BlockAccessList; - use ethrex_common::types::requests::compute_requests_hash; - use ethrex_common::types::{ - Block, BlockBody, BlockHeader, compute_transactions_root, compute_withdrawals_root, - }; - use ethrex_common::{Address, Bloom, H256}; - use ethrex_rlp::{decode::RLPDecode, encode::RLPEncode}; - - let payload = &req.execution_payload; - - let transactions = decode_payload_transactions(&payload.transactions)?; - let withdrawals = decode_payload_withdrawals(&payload.withdrawals); - - let block_access_list = BlockAccessList::decode(&payload.block_access_list) - .map_err(|e| format!("block access list decode: {e}"))?; - block_access_list - .validate_ordering() - .map_err(|e| format!("block access list ordering: {e}"))?; - if block_access_list.encode_to_vec().as_slice() != &payload.block_access_list[..] { - return Err("block access list is not canonically encoded".to_string()); - } - - let execution_requests = req.execution_requests.to_encoded_requests(); - let requests_hash = compute_requests_hash(&execution_requests); - let base_fee_per_gas = base_fee_per_gas_from_le_bytes(&payload.base_fee_per_gas)?; - let logs_bloom = Bloom::from_slice(&payload.logs_bloom); - - let transactions_root = compute_transactions_root(&transactions, crypto); - let withdrawals_root = compute_withdrawals_root(&withdrawals, crypto); - - let body = BlockBody { - transactions, - ommers: vec![], - withdrawals: Some(withdrawals), - }; - - let header = BlockHeader { - parent_hash: H256::from_slice(&payload.parent_hash), - ommers_hash: *DEFAULT_OMMERS_HASH, - coinbase: Address::from_slice(&payload.fee_recipient.0), - state_root: H256::from_slice(&payload.state_root), - transactions_root, - receipts_root: H256::from_slice(&payload.receipts_root), - logs_bloom, - difficulty: 0.into(), - number: payload.block_number, - gas_limit: payload.gas_limit, - gas_used: payload.gas_used, - timestamp: payload.timestamp, - extra_data: Bytes::copy_from_slice(&payload.extra_data), - prev_randao: H256::from_slice(&payload.prev_randao), - nonce: 0, - base_fee_per_gas: Some(base_fee_per_gas), - withdrawals_root: Some(withdrawals_root), - blob_gas_used: Some(payload.blob_gas_used), - excess_blob_gas: Some(payload.excess_blob_gas), - parent_beacon_block_root: Some(H256::from_slice(&req.parent_beacon_block_root)), - requests_hash: Some(requests_hash), - block_access_list_hash: Some(block_access_list.compute_hash(crypto)), - slot_number: Some(payload.slot_number), - ..Default::default() - }; - - let block = Block::new(header, body); - validate_reconstructed_block_hash(&block, &payload.block_hash, crypto)?; - Ok(block) -} - /// Validate that the blob versioned hashes in the `NewPayloadRequest` match /// the blob commitments in the block's transactions. fn validate_versioned_hashes<'a>( @@ -421,8 +70,7 @@ fn validate_versioned_hashes<'a>( /// /// Always compiled — used by the EXECUTE precompile path (`ethrex-blockchain`), /// the L2 advancer, and [`verify_stateless_block`]. Distinct from -/// [`eip8025_new_payload_request_to_block`], which reconstructs from the -/// EIP-8025 guest's `eip8025_ssz` payload (no in-SSZ block access list). +/// the pre-#3278 duplicate converter, which has been deleted. pub fn new_payload_request_to_block( req: ðrex_common::types::stateless_ssz::NewPayloadRequest, crypto: &dyn Crypto, @@ -533,8 +181,8 @@ pub fn new_payload_request_to_block( /// transaction so a guest can skip `ecrecover`; a key that does not derive to /// the recovered sender must reject the payload (issue #6716). /// -/// Hoisted out of the `eip8025_ssz` validation family, which was the only place -/// this check existed. Two consequences worth being explicit about: +/// Hoisted out of the now-deleted duplicate validation family, which was the +/// only place this check existed. Two consequences worth being explicit about: /// /// 1. The **guest** path must call this, or deduplicating the two validation /// families silently drops a spec-required check. @@ -554,11 +202,8 @@ pub fn new_payload_request_to_block( /// and both emit `successful_validation = false`, so the strict length check /// stays output-compatible with the reference — but it is compared before any /// zip so a short list fails cleanly rather than panicking. -/// Generic over the SSZ list bounds so it serves both the guest's -/// `PublicKeysList` and `stateless_ssz`'s field without a feature gate or a -/// duplicated alias. -pub fn validate_public_keys( - public_keys: &libssz_types::SszList, MAX_KEYS>, +pub fn validate_public_keys( + public_keys: &SszPublicKeys, block: ðrex_common::types::Block, crypto: &dyn Crypto, ) -> Result<(), ExecutionError> { @@ -600,18 +245,17 @@ pub fn validate_public_keys( /// (`StatelessExecutor`, the `StatelessValidator` trait impl invoked by the /// EXECUTE precompile). NOTE: the zkVM guest binaries do **not** call this — -/// they validate via the separate `validate_eip8025_*` path -/// (`eip8025_new_payload_request_to_block`), so changes here do not affect -/// zk-proof output. +/// the zkVM guest binaries now route here too, via [`run_stateless_guest`] — +/// there is no longer a separate duplicate validation family. /// /// Implements the `verify_stateless_new_payload` logic from execution-specs: /// reconstruct block → validate versioned hashes → execute statelessly → /// inject recomputed `burned_fees` → validate the recomputed block access list /// hash (Amsterdam+) → verify `block_hash`. /// -/// **Always compiled** — no `#[cfg(feature = "eip-8025")]` gate, so the -/// always-compiled `verify_inner` in `ethrex-blockchain` can call it without -/// pulling in the SSZ feature. +/// Always compiled: `verify_inner` in `ethrex-blockchain` calls this on the +/// EXECUTE precompile path, and the zkVM guest reaches it through +/// [`run_stateless_guest`]. pub fn verify_stateless_block( new_payload_request: ðrex_common::types::stateless_ssz::NewPayloadRequest, execution_witness: ethrex_common::types::block_execution_witness::ExecutionWitness, @@ -685,239 +329,139 @@ pub fn verify_stateless_block( Ok(()) } -#[cfg(feature = "eip-8025")] -fn canonical_execution_witness_to_rpc( - witness: CanonicalExecutionWitness, -) -> ethrex_common::types::block_execution_witness::RpcExecutionWitness { - use bytes::Bytes; - - fn copy_ssz_bytes( - bytes: &libssz_types::SszList, - ) -> Bytes { - Bytes::copy_from_slice(bytes) - } - - ethrex_common::types::block_execution_witness::RpcExecutionWitness { - state: witness.state.iter().map(copy_ssz_bytes).collect(), - // The specs do not have a `keys` field in the witness. This field - // is inherited from a legacy debug_executionWitness design. - // A `keys` field is not currently planned to be included in - // the specs. It might if there is rough consensus it is valuable - // for execution witness validation performance. - keys: Vec::new(), - codes: witness.codes.iter().map(copy_ssz_bytes).collect(), - headers: witness.headers.iter().map(copy_ssz_bytes).collect(), - } -} +/// Run the stateless validation guest: `statelessInputBytes` in, +/// `statelessOutputBytes` out. +/// +/// Never panics and never returns an error, mirroring `run_stateless_guest` in +/// `stateless_guest.py`. A **decode** failure commits the all-zero default. A +/// decodable input commits the real payload-request root, `chain_id` and +/// `schema_id` **even when validation fails** — zero sentinels are the +/// decode-failure signal only, and the root is computed before validation runs. +pub fn run_stateless_guest(input_bytes: &[u8], crypto: Arc) -> Vec { + use libssz::SszEncode; + + let Ok(input) = decode_stateless_input(input_bytes) else { + let mut out = Vec::new(); + SszStatelessValidationResult::default().ssz_append(&mut out); + return out; + }; -/// Validate the canonical input's `ChainConfig` and witness, then reconstruct -/// the `Block` from the Amsterdam `NewPayloadRequest` it carries and execute it -/// statelessly. -#[cfg(feature = "eip-8025")] -pub fn validate_eip8025_canonical_execution( - stateless_input: CanonicalStatelessInput, - chain_config: ethrex_common::types::ChainConfig, - crypto: Arc, -) -> Result<(), ExecutionError> { - let block_timestamp = stateless_input - .new_payload_request - .execution_payload - .timestamp; - let block_number = stateless_input + let new_payload_request_root = input .new_payload_request - .execution_payload - .block_number; - validate_canonical_chain_config( - &stateless_input.chain_config, - &chain_config, - block_number, - block_timestamp, - )?; - - let rpc_witness = canonical_execution_witness_to_rpc(stateless_input.witness); - // Decode headers once; reused by the chain-linkage check and `into_execution_witness`. - let decoded_headers = ethrex_common::types::block_execution_witness::decode_witness_headers( - &rpc_witness.headers, - )?; - // EELS `test_validation_headers_non_contiguous_chain`: check chain linkage - // in input order, before any sort/dedup. - ethrex_common::types::block_execution_witness::validate_witness_headers_chain( - &decoded_headers, - crypto.as_ref(), - )?; - - let execution_witness = rpc_witness.into_execution_witness( - chain_config, - block_number, - &decoded_headers, - crypto.as_ref(), - )?; + .hash_tree_root(&CryptoWrapper(crypto.clone())); + let chain_id = input.chain_id; - validate_eip8025_amsterdam_execution( - &stateless_input.new_payload_request, - execution_witness, - crypto, - stateless_input.public_keys, - ) -} + let successful_validation = validate_stateless_execution(&input, crypto).is_ok(); -/// Validate `chain_id`, `active_fork.activation`, and `active_fork.blob_schedule` -/// from the prover's `CanonicalChainConfig` against the verifier's `ChainConfig`. -#[cfg(feature = "eip-8025")] -fn validate_canonical_chain_config( - canonical: &crate::l1::input::CanonicalChainConfig, - expected: ðrex_common::types::ChainConfig, - block_number: u64, - block_timestamp: u64, -) -> Result<(), ExecutionError> { - if canonical.chain_id != expected.chain_id { - return Err(ExecutionError::Internal(format!( - "chain_id mismatch between canonical input ({}) and chain config ({})", - canonical.chain_id, expected.chain_id - ))); - } - - // EELS `validate_chain_config` / `_is_activation_active`: the declared active - // fork must actually be active for this payload. The activation point must set - // a block_number or a timestamp, and the payload must be at or past it. - // `block_number`/`timestamp` are `SszList`, - // i.e. an `Option` carrying 0 or 1 value. - let activation = &canonical.active_fork.activation; - let activation_block_number = activation.block_number.iter().next().copied(); - let activation_timestamp = activation.timestamp.iter().next().copied(); - if activation_block_number.is_none() && activation_timestamp.is_none() { - return Err(ExecutionError::Internal( - "fork activation must set block_number or timestamp".to_string(), - )); - } - if let Some(activation_block_number) = activation_block_number - && block_number < activation_block_number - { - return Err(ExecutionError::Internal(format!( - "ChainConfig active_fork is not active for the target payload: \ - block_number {block_number} precedes activation {activation_block_number}" - ))); - } - if let Some(activation_timestamp) = activation_timestamp - && block_timestamp < activation_timestamp - { - return Err(ExecutionError::Internal(format!( - "ChainConfig active_fork is not active for the target payload: \ - timestamp {block_timestamp} precedes activation {activation_timestamp}" - ))); + let mut out = Vec::new(); + SszStatelessValidationResult { + new_payload_request_root, + successful_validation, + chain_id, + schema_id: STATELESS_INPUT_SCHEMA_ID, } - - // As of glamsterdam-devnet-7, `SszForkConfig` carries only `activation` — the - // `fork` id and per-fork `blob_schedule` fields were dropped from the canonical - // input, so there is nothing further to cross-check against `expected` here - // beyond the chain id and activation already validated above. - - Ok(()) + .ssz_append(&mut out); + out } -/// Reconstruct the `Block` from a legacy `NewPayloadRequest` and execute it -/// statelessly against the supplied `ExecutionWitness`. -#[cfg(feature = "eip-8025")] -pub fn validate_eip8025_execution( - new_payload_request: ðrex_common::types::eip8025_ssz::NewPayloadRequest, - execution_witness: ethrex_common::types::block_execution_witness::ExecutionWitness, +/// Validate a decoded stateless input: rebuild the `ExecutionWitness`, derive the +/// `ChainConfig` from `(chain_id, Amsterdam)`, check the supplied public keys, and +/// execute the payload statelessly. +pub fn validate_stateless_execution( + input: &SszStatelessInput, crypto: Arc, ) -> Result<(), ExecutionError> { - // Transform SSZ NewPayloadRequest → Block - let block = eip8025_new_payload_request_to_block(new_payload_request, crypto.as_ref()) - .map_err(|e| ExecutionError::Internal(format!("payload conversion: {e}")))?; - - validate_reconstructed_block_hash( - &block, - &new_payload_request.execution_payload.block_hash, - crypto.as_ref(), - ) - .map_err(|e| ExecutionError::Internal(format!("payload conversion: {e}")))?; + let execution_witness = + ethrex_common::types::block_execution_witness::ExecutionWitness::from_ssz(input) + .map_err(|e| ExecutionError::Internal(format!("witness rebuild: {e}")))?; - // Validate blob versioned hashes - validate_versioned_hashes(&block, new_payload_request.versioned_hashes.iter())?; - - // Execute statelessly — reuse the common `execute_blocks` infrastructure - let _result = execute_blocks( - &[block], - execution_witness, - ELASTICITY_MULTIPLIER, - |db, _| Ok(Evm::new_for_l1(db.clone(), crypto.clone())), - crypto.clone(), - )?; + // Reconstruct the block once so the public keys can be checked against its + // recovered senders before committing to execution. + let block = new_payload_request_to_block(&input.new_payload_request, crypto.as_ref()) + .map_err(|e| ExecutionError::Internal(format!("payload conversion: {e}")))?; + validate_public_keys(&input.public_keys, &block, crypto.as_ref())?; - Ok(()) + verify_stateless_block(&input.new_payload_request, execution_witness, crypto) } -#[cfg(feature = "eip-8025")] -fn validate_eip8025_amsterdam_execution( - new_payload_request: ðrex_common::types::eip8025_ssz::NewPayloadRequestAmsterdam, +/// Validate blocks statelessly against an in-memory witness. +/// +/// The spec entrypoint is [`run_stateless_guest`]; this exists for callers that +/// hold a witness they generated themselves — the ef_tests witness-sufficiency +/// checks — rather than spec wire bytes. It commits to nothing. +pub fn validate_blocks_statelessly( + blocks: &[ethrex_common::types::Block], execution_witness: ethrex_common::types::block_execution_witness::ExecutionWitness, crypto: Arc, - public_keys: PublicKeysList, ) -> Result<(), ExecutionError> { - let block = new_payload_request_amsterdam_to_block(new_payload_request, crypto.as_ref()) - .map_err(|e| ExecutionError::Internal(format!("payload conversion: {e}")))?; - - validate_versioned_hashes(&block, new_payload_request.versioned_hashes.iter())?; - - if public_keys.len() != block.body.transactions.len() { - return Err(ExecutionError::Internal(format!( - "Found {} public keys in the stateless input, but there are {} transactions", - public_keys.len(), - block.body.transactions.len() - ))); - } - for (public_key, tx) in public_keys.iter().zip(block.body.transactions.iter()) { - // SSZ decode fixes the length at 65; uncompressed secp256k1 is 0x04 || X || Y. - let pk_bytes: &[u8] = public_key; - if pk_bytes[0] != 0x04 { - return Err(ExecutionError::Internal( - "Stateless input public key is not a 65-byte uncompressed secp256k1 key" - .to_string(), - )); - } - let derived = Address::from_slice(&keccak(&pk_bytes[1..])[12..]); - let recovered = tx.sender(crypto.as_ref()).map_err(|e| { - ExecutionError::Internal(format!("failed to recover transaction sender: {e}")) - })?; - if recovered != derived { - return Err(ExecutionError::Internal( - "Stateless input public key does not match recovered transaction sender" - .to_string(), - )); - } - } - - let _result = execute_blocks( - &[block], + execute_blocks( + blocks, execution_witness, ELASTICITY_MULTIPLIER, |db, _| Ok(Evm::new_for_l1(db.clone(), crypto.clone())), crypto.clone(), )?; - Ok(()) } -#[cfg(all(test, feature = "eip-8025"))] +#[cfg(test)] mod tests { - use std::sync::Arc; + use super::*; + use crate::crypto::NativeCrypto; + use libssz::SszEncode; + + fn default_result_bytes() -> Vec { + let mut out = Vec::new(); + SszStatelessValidationResult::default().ssz_append(&mut out); + out + } - use crate::{common::ExecutionError, crypto::NativeCrypto, l1::execution_program}; + /// A guest must never panic on hostile input, and a decode failure commits the + /// all-zero result rather than a partially-filled one. + #[test] + fn malformed_input_yields_default_result() { + let crypto = Arc::new(NativeCrypto); + let expected = default_result_bytes(); + + for bytes in [ + vec![], // no schema id at all + vec![0x15], // half a schema id + vec![0x15, 0x01], // right id, empty body + vec![0x15, 0x02, 0x00], // right fork, wrong revision + vec![0x16, 0x01, 0x00], // wrong fork index + vec![0x15, 0x01, 0xde, 0xad], // right id, garbage body + ] { + assert_eq!( + run_stateless_guest(&bytes, crypto.clone()), + expected, + "input {bytes:?} must produce the default result" + ); + } + } + /// The output is fully fixed-size under #3278: 32 + 1 + 8 + 2. #[test] - fn execution_program_rejects_invalid_eip8025_wire_bytes() { - let err = match execution_program(&[], Arc::new(NativeCrypto)) { - Ok(_) => panic!("expected invalid EIP-8025 input to fail decoding"), - Err(err) => err, - }; + fn default_result_is_43_zero_bytes() { + let encoded = default_result_bytes(); + assert_eq!(encoded.len(), 43, "result must be fixed-size"); + assert!( + encoded.iter().all(|b| *b == 0), + "a decode failure commits all zeros, including schema_id" + ); + } - match err { - ExecutionError::Internal(msg) => { - assert_eq!(msg, "failed to decode EIP-8025 input: input too short"); - } - other => panic!("expected internal decode error, got {other:?}"), + /// Only `0x1501` decodes — the guest's entire fork check, so the one + /// rejection that must not regress. + #[test] + fn only_amsterdam_schema_id_decodes() { + assert_eq!(STATELESS_INPUT_SCHEMA_ID, 0x1501); + for id in [0x1401u16, 0x1502, 0x1601, 0x0000] { + let mut bytes = id.to_be_bytes().to_vec(); + bytes.extend_from_slice(&[0u8; 8]); + assert!( + decode_stateless_input(&bytes).is_err(), + "schema id {id:#06x} must be rejected" + ); } } } diff --git a/crates/guest-program/src/l2/mod.rs b/crates/guest-program/src/l2/mod.rs index ac2ed29b6df..c8798205ccd 100644 --- a/crates/guest-program/src/l2/mod.rs +++ b/crates/guest-program/src/l2/mod.rs @@ -8,4 +8,4 @@ mod program; pub use error::L2ExecutionError; pub use input::ProgramInput; pub use output::ProgramOutput; -pub use program::execution_program; +pub use program::{execution_program, run_guest}; diff --git a/crates/guest-program/src/l2/program.rs b/crates/guest-program/src/l2/program.rs index a4d5b83475a..be18ed900a1 100644 --- a/crates/guest-program/src/l2/program.rs +++ b/crates/guest-program/src/l2/program.rs @@ -76,3 +76,16 @@ pub fn execution_program( balance_diffs, }) } + +/// Run the L2 batch guest: rkyv-encoded `ProgramInput` in, encoded +/// `ProgramOutput` out. +/// +/// Mirrors `crate::l1::run_stateless_guest`'s byte-in/byte-out shape so the zkVM +/// binaries carry no serialization logic, while keeping the L2 commitment format — +/// the on-chain verifier needs state roots and blob/message commitments that +/// `statelessOutputBytes` does not carry. +pub fn run_guest(input_bytes: &[u8], crypto: Arc) -> Result, L2ExecutionError> { + let input = rkyv::from_bytes::(input_bytes) + .map_err(|e| L2ExecutionError::Internal(format!("rkyv decode: {e}")))?; + Ok(execution_program(input, crypto)?.encode()) +} diff --git a/crates/guest-program/src/lib.rs b/crates/guest-program/src/lib.rs index e441732e917..8d0833ef257 100644 --- a/crates/guest-program/src/lib.rs +++ b/crates/guest-program/src/lib.rs @@ -4,9 +4,15 @@ pub mod l1; pub mod l2; pub mod methods; -// Backward-compatible re-exports based on feature flag. -// The prover backend uses `ethrex_guest_program::input::ProgramInput`, etc. -// These re-exports allow existing code to work without changes. +// Input/output aliases, selected by the `l2` feature. +// +// The L2 batch prover keeps its own rkyv-serialized `ProgramInput` and its own +// commitment shape — the on-chain verifier needs state roots and blob/message +// commitments that `statelessOutputBytes` does not carry. +// +// The L1 guest's input is the spec's `statelessInputBytes`: an opaque blob the +// host passes straight through, aliased here so `ProverBackend` stays +// generic-free. Its output is the spec's `SszStatelessValidationResult`. #[cfg(feature = "l2")] pub mod input { @@ -14,7 +20,7 @@ pub mod input { } #[cfg(not(feature = "l2"))] pub mod input { - pub use crate::l1::ProgramInput; + pub type ProgramInput = Vec; } #[cfg(feature = "l2")] @@ -23,17 +29,13 @@ pub mod output { } #[cfg(not(feature = "l2"))] pub mod output { - pub use crate::l1::ProgramOutput; + pub use ethrex_common::types::stateless_ssz::SszStatelessValidationResult as ProgramOutput; } #[cfg(feature = "l2")] pub mod execution { pub use crate::l2::execution_program; } -#[cfg(not(feature = "l2"))] -pub mod execution { - pub use crate::l1::execution_program; -} // When running clippy, the ELFs are not built, so we define them empty. diff --git a/crates/prover/Cargo.toml b/crates/prover/Cargo.toml index 6210e0dffaa..8c68d738fca 100644 --- a/crates/prover/Cargo.toml +++ b/crates/prover/Cargo.toml @@ -21,6 +21,7 @@ spawned-concurrency.workspace = true # ethrex ethrex-common.workspace = true +libssz.workspace = true ethrex-vm.workspace = true ethrex-rlp.workspace = true @@ -65,7 +66,6 @@ profiling = ["sp1-sdk?/profiling"] gpu = ["risc0-zkvm?/cuda", "sp1-sdk?/cuda", "openvm-sdk?/cuda"] l2 = ["ethrex-guest-program/l2"] -eip-8025 = ["ethrex-guest-program/eip-8025", "ethrex-common/eip-8025"] # temporary feature until we fix cargo-zisk setup-rom from failing in the CI ci = ["ethrex-guest-program/ci"] diff --git a/crates/prover/src/backend/exec.rs b/crates/prover/src/backend/exec.rs index 6e53b01ba56..ed3d534335d 100644 --- a/crates/prover/src/backend/exec.rs +++ b/crates/prover/src/backend/exec.rs @@ -21,30 +21,33 @@ impl ExecBackend { Self } - /// Core execution - runs the guest program directly. + /// Core execution - runs the L1 stateless validator directly. + /// + /// `ProgramInput` is the spec's `statelessInputBytes`, so this is the same + /// entrypoint the released guest ELF runs. `successful_validation = false` + /// surfaces as `Err` so result-only callers (ef_tests) treat it as failure. + #[cfg(not(feature = "l2"))] fn execute_core(input: ProgramInput) -> Result { + use libssz::SszDecode; + let crypto = Arc::new(NativeCrypto); - // L1 EIP-8025 `execution_program` takes raw bytes, not `ProgramInput`. - // When `l2` is also on, the re-exported types are the L2 shape and the - // standard `execution_program(input, crypto)` path applies instead. - #[cfg(all(feature = "eip-8025", not(feature = "l2")))] - { - let output = ethrex_guest_program::l1::execute_decoded(input, crypto) - .map_err(BackendError::execution)?; - // Surface `valid = false` as Err so result-only callers (e.g. ef_tests) - // treat it as execution failure, matching the legacy path's semantics. - if !output.valid { - return Err(BackendError::execution( - "eip-8025 stateless execution: valid=false", - )); - } - Ok(output) - } - #[cfg(any(not(feature = "eip-8025"), feature = "l2"))] - { - ethrex_guest_program::execution::execution_program(input, crypto) - .map_err(BackendError::execution) + let output_bytes = ethrex_guest_program::l1::run_stateless_guest(&input, crypto); + let output = ProgramOutput::from_ssz_bytes(&output_bytes) + .map_err(|e| BackendError::execution(format!("output decode: {e:?}")))?; + if !output.successful_validation { + return Err(BackendError::execution( + "stateless validation returned successful_validation = false", + )); } + Ok(output) + } + + /// Core execution - runs the L2 batch guest directly. + #[cfg(feature = "l2")] + fn execute_core(input: ProgramInput) -> Result { + let crypto = Arc::new(NativeCrypto); + ethrex_guest_program::execution::execution_program(input, crypto) + .map_err(BackendError::execution) } fn empty_proof_bytes() -> ProverOutput { @@ -83,17 +86,9 @@ impl ProverBackend for ExecBackend { input: ProgramInput, _format: ProofFormat, ) -> Result { - // The `Direct` variant returns a zero `new_payload_request_root` sentinel - // that callers must not interpret as a real commitment. `execute()` is - // fine (discards the output) but `prove()` exposes it. - // Only the L1 EIP-8025 `ProgramInput` carries the `Direct` variant; when - // `l2` is also on, `ProgramInput` is the L2 shape and this guard is inert. - #[cfg(all(feature = "eip-8025", not(feature = "l2")))] - if matches!(input, ProgramInput::Direct { .. }) { - return Err(BackendError::execution( - "ExecBackend::prove does not accept ProgramInput::Direct (test-only path)", - )); - } + // The old `ProgramInput::Direct` guard is gone with the variant: every L1 + // input is now real `statelessInputBytes`, so the zero-root sentinel it + // protected against cannot arise. warn!("\"exec\" prover backend generates no proof, only executes"); Self::execute_core(input) } diff --git a/crates/prover/src/backend/openvm.rs b/crates/prover/src/backend/openvm.rs index cea8e08f6ed..ecf23f68865 100644 --- a/crates/prover/src/backend/openvm.rs +++ b/crates/prover/src/backend/openvm.rs @@ -69,8 +69,15 @@ impl ProverBackend for OpenVmBackend { } fn serialize_input(&self, input: &ProgramInput) -> Result { - let mut stdin = StdIn::default(); + // On the L1 path `ProgramInput` IS the spec's `statelessInputBytes`, so it + // must reach the guest byte-for-byte — rkyv-wrapping it would make the + // guest's schema-prefix check fail. Only the L2 batch input is rkyv. + #[cfg(feature = "l2")] let bytes = rkyv::to_bytes::(input).map_err(BackendError::serialization)?; + #[cfg(not(feature = "l2"))] + let bytes = input; + + let mut stdin = StdIn::default(); stdin.write_bytes(bytes.as_slice()); Ok(stdin) } diff --git a/crates/prover/src/backend/risc0.rs b/crates/prover/src/backend/risc0.rs index 19740484154..d3cc66603ac 100644 --- a/crates/prover/src/backend/risc0.rs +++ b/crates/prover/src/backend/risc0.rs @@ -91,7 +91,14 @@ impl ProverBackend for Risc0Backend { } fn serialize_input(&self, input: &ProgramInput) -> Result { + // On the L1 path `ProgramInput` IS the spec's `statelessInputBytes`, so it + // must reach the guest byte-for-byte — rkyv-wrapping it would make the + // guest's schema-prefix check fail. Only the L2 batch input is rkyv. + #[cfg(feature = "l2")] let bytes = rkyv::to_bytes::(input).map_err(BackendError::serialization)?; + #[cfg(not(feature = "l2"))] + let bytes = input; + ExecutorEnv::builder() .write_slice(bytes.as_slice()) .build() diff --git a/crates/prover/src/backend/sp1.rs b/crates/prover/src/backend/sp1.rs index 9a258d14529..5a18adcd5a9 100644 --- a/crates/prover/src/backend/sp1.rs +++ b/crates/prover/src/backend/sp1.rs @@ -143,8 +143,15 @@ impl ProverBackend for Sp1Backend { } fn serialize_input(&self, input: &ProgramInput) -> Result { - let mut stdin = SP1Stdin::new(); + // On the L1 path `ProgramInput` IS the spec's `statelessInputBytes`, so it + // must reach the guest byte-for-byte — rkyv-wrapping it would make the + // guest's schema-prefix check fail. Only the L2 batch input is rkyv. + #[cfg(feature = "l2")] let bytes = rkyv::to_bytes::(input).map_err(BackendError::serialization)?; + #[cfg(not(feature = "l2"))] + let bytes = input; + + let mut stdin = SP1Stdin::new(); stdin.write_slice(bytes.as_slice()); Ok(stdin) } diff --git a/crates/prover/src/backend/zisk.rs b/crates/prover/src/backend/zisk.rs index 68f4e5b4bd9..a787cc73f9f 100644 --- a/crates/prover/src/backend/zisk.rs +++ b/crates/prover/src/backend/zisk.rs @@ -135,8 +135,14 @@ impl ProverBackend for ZiskBackend { } fn serialize_input(&self, input: &ProgramInput) -> Result { + // On the L1 path `ProgramInput` IS the spec's `statelessInputBytes`, so it + // must reach the guest byte-for-byte — rkyv-wrapping it would make the + // guest's schema-prefix check fail. Only the L2 batch input is rkyv. + #[cfg(feature = "l2")] let input_bytes = rkyv::to_bytes::(input).map_err(BackendError::serialization)?; + #[cfg(not(feature = "l2"))] + let input_bytes = input; // ZisK expects input in ZiskStdin format: an 8-byte little-endian length // prefix, the data, then zero-padding to 8-byte alignment. The guest reads diff --git a/crates/prover/src/lib.rs b/crates/prover/src/lib.rs index a84cfc5853b..ca27a352031 100644 --- a/crates/prover/src/lib.rs +++ b/crates/prover/src/lib.rs @@ -1,16 +1,3 @@ -// Non-exec backends rkyv-serialize `ProgramInput`, which the `eip-8025` variant -// doesn't implement. -#[cfg(all( - feature = "eip-8025", - any( - feature = "sp1", - feature = "risc0", - feature = "openvm", - feature = "zisk" - ) -))] -compile_error!("feature `eip-8025` cannot be combined with `sp1`, `risc0`, `openvm`, or `zisk`"); - pub mod backend; pub mod protocol; pub mod prover; diff --git a/crates/vm/Cargo.toml b/crates/vm/Cargo.toml index 44230b3901a..8b70b606b2b 100644 --- a/crates/vm/Cargo.toml +++ b/crates/vm/Cargo.toml @@ -37,7 +37,6 @@ default = ["secp256k1", "rayon"] rayon = ["dep:rayon", "ethrex-levm/rayon"] secp256k1 = ["ethrex-levm/secp256k1", "ethrex-common/secp256k1"] c-kzg = ["ethrex-levm/c-kzg", "ethrex-common/c-kzg"] -eip-8025 = ["ethrex-common/eip-8025"] sp1 = ["ethrex-levm/sp1", "ethrex-common/sp1"] risc0 = ["ethrex-levm/risc0", "ethrex-common/risc0", "c-kzg"] diff --git a/tooling/Cargo.lock b/tooling/Cargo.lock index d7f9064a8b5..e6bba372166 100644 --- a/tooling/Cargo.lock +++ b/tooling/Cargo.lock @@ -1639,19 +1639,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "bls12_381" -version = "0.8.0" -source = "git+https://github.com/lambdaclass/bls12_381?branch=expose-affine-constructors#78cad0378b17fc3157b83f514be192bf46edf9a1" -dependencies = [ - "digest 0.10.7", - "ff 0.13.1", - "group 0.13.0", - "pairing 0.23.0", - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "blst" version = "0.3.16" @@ -3421,7 +3408,6 @@ dependencies = [ name = "ethrex-guest-program" version = "23.0.0" dependencies = [ - "bls12_381 0.8.0", "bytes", "ethereum-types", "ethrex-common 23.0.0", @@ -3429,9 +3415,7 @@ dependencies = [ "ethrex-l2-common", "ethrex-rlp 23.0.0", "ethrex-vm", - "ff 0.13.1", "hex", - "k256 0.13.4 (registry+https://github.com/rust-lang/crates.io-index)", "libssz", "libssz-derive", "libssz-merkle", @@ -3439,9 +3423,6 @@ dependencies = [ "rkyv", "serde", "serde_with", - "sp1-build", - "sp1-sdk", - "substrate-bn", "thiserror 2.0.18", ] @@ -3700,11 +3681,10 @@ dependencies = [ "ethrex-guest-program", "ethrex-rlp 23.0.0", "ethrex-vm", + "libssz", "rkyv", "serde", "serde_json", - "sp1-prover", - "sp1-sdk", "spawned-concurrency", "thiserror 2.0.18", "tokio", @@ -5261,7 +5241,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a575df5f985fe1cd5b2b05664ff6accfc46559032b954529fd225a2168d27b0f" dependencies = [ "bitvec", - "bls12_381 0.7.1", + "bls12_381", "ff 0.12.1", "group 0.12.1", "rand_core 0.6.4", @@ -9676,19 +9656,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "substrate-bn" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b5bbfa79abbae15dd642ea8176a21a635ff3c00059961d1ea27ad04e5b441c" -dependencies = [ - "byteorder", - "crunchy", - "lazy_static", - "rand 0.8.5", - "rustc-hex", -] - [[package]] name = "subtle" version = "2.6.1" @@ -11635,7 +11602,7 @@ dependencies = [ "ark-std 0.4.0", "bitvec", "blake2", - "bls12_381 0.7.1", + "bls12_381", "byteorder", "cfg-if", "group 0.12.1", diff --git a/tooling/ef_tests/blockchain/Cargo.toml b/tooling/ef_tests/blockchain/Cargo.toml index c8e351444a9..535bf57b7d1 100644 --- a/tooling/ef_tests/blockchain/Cargo.toml +++ b/tooling/ef_tests/blockchain/Cargo.toml @@ -32,12 +32,10 @@ path = "./lib.rs" [features] default = ["c-kzg"] c-kzg = ["ethrex-blockchain/c-kzg"] -sp1 = ["ethrex-guest-program/sp1-build-elf", "ethrex-prover/sp1"] -stateless = [ - "ethrex-common/eip-8025", - "ethrex-guest-program/eip-8025", - "ethrex-prover/eip-8025", -] +# Selects the conformance-vector fixture root and the stateless test set in +# tests/all.rs. No longer propagates a cargo feature: stateless validation is +# unconditional since the feature flag was removed. +stateless = [] l2 = ["ethrex-guest-program/l2", "ethrex-prover/l2"] [[test]] diff --git a/tooling/ef_tests/blockchain/Makefile b/tooling/ef_tests/blockchain/Makefile index f0201bb7704..98fc66e2680 100644 --- a/tooling/ef_tests/blockchain/Makefile +++ b/tooling/ef_tests/blockchain/Makefile @@ -1,4 +1,4 @@ -.PHONY: download-test-vectors clean-vectors test test-levm test-sp1 test-stateless amsterdam-vectors zkevm-vectors test-stateless-zkevm stateless-vector +.PHONY: download-test-vectors clean-vectors test test-levm test-stateless amsterdam-vectors zkevm-vectors test-stateless-zkevm stateless-vector VECTORS_ROOT := vectors FIXTURES_FILE := ../.fixtures_url @@ -86,9 +86,6 @@ clean-vectors: ## 🗑️ Clean test vectors test-levm: $(VECTORS_TARGETS) amsterdam-vectors ## 🧪 Run blockchain tests with LEVM cargo test --profile release-fast -test-sp1: $(VECTORS_TARGETS) amsterdam-vectors - cargo test --profile release-fast --features sp1 - test-stateless: zkevm-vectors cargo test --profile release-fast --features stateless diff --git a/tooling/ef_tests/blockchain/test_runner.rs b/tooling/ef_tests/blockchain/test_runner.rs index 1638eb91c5f..3ecee579b7c 100644 --- a/tooling/ef_tests/blockchain/test_runner.rs +++ b/tooling/ef_tests/blockchain/test_runner.rs @@ -20,10 +20,6 @@ use ethrex_common::{ InvalidBlockHeaderError, }, }; -use ethrex_guest_program::input::ProgramInput; -#[cfg(feature = "sp1")] -use ethrex_prover::Sp1Backend; -use ethrex_prover::{BackendType, ExecBackend, ProverBackend}; use ethrex_rlp::decode::RLPDecode; use ethrex_storage::{EngineType, Store}; use ethrex_vm::EvmError; @@ -48,7 +44,7 @@ fn merkle_pool() -> Arc { pub fn parse_and_execute( path: &Path, skipped_tests: Option<&[&str]>, - stateless_backend: Option, + run_stateless: bool, ) -> datatest_stable::Result<()> { let rt = tokio::runtime::Runtime::new().unwrap(); let tests = parse_tests(path); @@ -66,7 +62,7 @@ pub fn parse_and_execute( // bundle filled with the new predeploy addresses is released and // `.fixtures_url_zkevm` is bumped. See docs/known_issues.md. let skip_stateless_amsterdam = - stateless_backend.is_some() && test.network >= Fork::Amsterdam; + run_stateless && test.network >= Fork::Amsterdam; let should_skip_test = test.network < Fork::Merge || skip_stateless_amsterdam || skipped_tests @@ -77,7 +73,7 @@ pub fn parse_and_execute( continue; } - let result = rt.block_on(run_ef_test(&test_key, &test, stateless_backend)); + let result = rt.block_on(run_ef_test(&test_key, &test, run_stateless)); if let Err(e) = result { eprintln!("Test {test_key} failed: {e:?}"); @@ -96,7 +92,7 @@ pub fn parse_and_execute( pub async fn run_ef_test( test_key: &str, test: &TestUnit, - stateless_backend: Option, + run_stateless: bool, ) -> Result<(), String> { // check that the decoded genesis block header matches the deserialized one let genesis_rlp = test.genesis_rlp.clone(); @@ -141,7 +137,11 @@ pub async fn run_ef_test( // Run stateless if backend was specified for this. // TODO: See if we can run stateless without needing a previous run. We can't easily do it for now. #4142 - if let Some(backend) = stateless_backend { + // The `stateless_backend` option now only selects *whether* to run stateless + // validation, not which backend: the in-memory paths go through + // `validate_blocks_statelessly` and the wire path through the guest + // entrypoint, neither of which is backend-dispatched. + if run_stateless { // Use the fixture's witness when present (either `executionWitness` or // `statelessInputBytes`); otherwise regenerate by re-running execution. #[cfg(feature = "stateless")] @@ -152,13 +152,12 @@ pub async fn run_ef_test( }) }); if has_fixture_witness { - run_stateless_from_fixture(test, test_key, backend).await?; - check_witness_generation_against_fixture(&blockchain, test, test_key, backend) - .await?; + run_stateless_from_fixture(test, test_key).await?; + check_witness_generation_against_fixture(&blockchain, test, test_key).await?; return Ok(()); } } - re_run_stateless(blockchain, test, test_key, backend).await?; + re_run_stateless(blockchain, test, test_key).await?; }; Ok(()) @@ -552,7 +551,6 @@ async fn re_run_stateless( blockchain: Blockchain, test: &TestUnit, test_key: &str, - backend_type: BackendType, ) -> Result<(), String> { let blocks = test .blocks @@ -575,13 +573,14 @@ async fn re_run_stateless( // At this point witness is guaranteed to be Ok let execution_witness = witness.unwrap(); - let program_input = ProgramInput::new(blocks, execution_witness); - - let execute_result = match backend_type { - BackendType::Exec => ExecBackend::new().execute(program_input), - #[cfg(feature = "sp1")] - BackendType::SP1 => Sp1Backend::new().execute(program_input), - }; + // A generated witness has no spec wire bytes, so this cannot go through the + // byte entrypoint; `ExecBackend` was also the only backend able to run an + // in-memory witness, so the dispatch is gone with it. + let execute_result = ethrex_guest_program::l1::validate_blocks_statelessly( + &blocks, + execution_witness, + std::sync::Arc::new(ethrex_crypto::NativeCrypto), + ); if let Err(e) = execute_result { if !test_should_fail { @@ -606,7 +605,6 @@ async fn re_run_stateless( async fn run_stateless_from_fixture( test: &TestUnit, test_key: &str, - backend_type: BackendType, ) -> Result<(), String> { let chain_config = test.network.chain_config(); @@ -631,17 +629,16 @@ async fn run_stateless_from_fixture( })?, }; - // Prefer the canonical EIP-8025 wire path (production guest binary entry - // point) which exercises the public_keys / hash_tree_root checks the - // legacy `ProgramInput` route bypasses. - if let Some(input_hex) = block_data.stateless_input_bytes.as_deref() { - run_stateless_from_input_bytes( - test_key, - &test.network, - block_number, - input_hex, - expected_valid, - )?; + // Prefer the spec wire path — the same entrypoint the released guest ELF + // runs — and compare the whole 43-byte result rather than peeking at the + // validity byte. Only blocks that carry BOTH the input and the expected + // output can go this way; the rest fall through to the witness route + // below, which is why `parse_expected_valid_flag` is still needed. + if let (Some(input_hex), Some(output_hex)) = ( + block_data.stateless_input_bytes.as_deref(), + block_data.stateless_output_bytes.as_deref(), + ) { + run_stateless_from_input_bytes(test_key, block_number, input_hex, output_hex)?; continue; } @@ -674,12 +671,11 @@ async fn run_stateless_from_fixture( format!("witness conversion failed for {test_key} block {block_number}: {e}") })?; - let program_input = ProgramInput::new(vec![block], execution_witness); - let exec_result = match backend_type { - BackendType::Exec => ExecBackend::new().execute(program_input), - #[cfg(feature = "sp1")] - BackendType::SP1 => Sp1Backend::new().execute(program_input), - }; + let exec_result = ethrex_guest_program::l1::validate_blocks_statelessly( + std::slice::from_ref(&block), + execution_witness, + std::sync::Arc::new(ethrex_crypto::NativeCrypto), + ); match (expected_valid, exec_result) { (true, Ok(_)) | (false, Err(_)) => {} @@ -715,7 +711,6 @@ async fn check_witness_generation_against_fixture( blockchain: &Blockchain, test: &TestUnit, test_key: &str, - backend_type: BackendType, ) -> Result<(), String> { use std::collections::BTreeSet; @@ -762,12 +757,11 @@ async fn check_witness_generation_against_fixture( // Sufficiency: the generated witness must support stateless re-execution // of the block on its own, independent of how close it is to canonical. - let program_input = ProgramInput::new(vec![block], generated_witness.clone()); - let exec_result = match backend_type { - BackendType::Exec => ExecBackend::new().execute(program_input), - #[cfg(feature = "sp1")] - BackendType::SP1 => Sp1Backend::new().execute(program_input), - }; + let exec_result = ethrex_guest_program::l1::validate_blocks_statelessly( + std::slice::from_ref(&block), + generated_witness.clone(), + std::sync::Arc::new(ethrex_crypto::NativeCrypto), + ); if let Err(e) = exec_result { errors.push(format!( "{test_key} block {block_number}: generated witness INSUFFICIENT for \ @@ -868,42 +862,33 @@ fn describe_witness_item(section: &str, bytes: &[u8]) -> String { #[cfg(feature = "stateless")] fn run_stateless_from_input_bytes( test_key: &str, - test_network: &Fork, block_number: u64, input_hex: &str, - expected_valid: bool, + expected_output_hex: &str, ) -> Result<(), String> { - use ethrex_guest_program::l1::{DecodedEip8025, decode_canonical_stateless_input_bytes}; - - let trimmed = input_hex.strip_prefix("0x").unwrap_or(input_hex); - let bytes = hex::decode(trimmed).map_err(|e| { - format!("statelessInputBytes hex decode failed for {test_key} block {block_number}: {e}") - })?; + use ethrex_guest_program::l1::run_stateless_guest; - // Decode failures count as the canonical-input rejection path: a negative - // fixture with malformed top-level SSZ should still match `expected_valid=false`. - let exec_result = match decode_canonical_stateless_input_bytes(&bytes) { - Ok(stateless_input) => { - let chain_config = *test_network.chain_config(); - let program_input = ProgramInput::wire(DecodedEip8025::Canonical { - stateless_input, - chain_config, - }); - ExecBackend::new().execute(program_input) - } - Err(e) => Err(ethrex_prover::BackendError::execution(format!( - "statelessInputBytes decode failed: {e}" - ))), + let decode = |label: &str, hex_str: &str| { + let trimmed = hex_str.strip_prefix("0x").unwrap_or(hex_str); + hex::decode(trimmed).map_err(|e| { + format!("{label} hex decode failed for {test_key} block {block_number}: {e}") + }) }; - match (expected_valid, exec_result) { - (true, Ok(_)) | (false, Err(_)) => Ok(()), - (true, Err(e)) => Err(format!( - "Stateless execution failed for {test_key} block {block_number} but fixture expected it to succeed: {e}" - )), - (false, Ok(_)) => Err(format!( - "Stateless execution succeeded for {test_key} block {block_number} but fixture expected it to fail (invalid statelessInputBytes)" - )), + + let input = decode("statelessInputBytes", input_hex)?; + let expected = decode("statelessOutputBytes", expected_output_hex)?; + + let actual = run_stateless_guest(&input, std::sync::Arc::new(ethrex_crypto::NativeCrypto)); + + if actual != expected { + return Err(format!( + "statelessOutputBytes mismatch for {test_key} block {block_number}:\n \ + expected 0x{}\n actual 0x{}", + hex::encode(&expected), + hex::encode(&actual), + )); } + Ok(()) } /// Decode the `valid` byte (index 32) from a zkevm-fixture `statelessOutputBytes` hex diff --git a/tooling/ef_tests/blockchain/tests/all.rs b/tooling/ef_tests/blockchain/tests/all.rs index 1f0ce85be56..6a293861da7 100644 --- a/tooling/ef_tests/blockchain/tests/all.rs +++ b/tooling/ef_tests/blockchain/tests/all.rs @@ -1,14 +1,9 @@ use ef_tests_blockchain::test_runner::parse_and_execute; -use ethrex_prover::backend::BackendType; use std::path::Path; -// Enable only one of `sp1` or `stateless` at a time. -#[cfg(all(feature = "sp1", feature = "stateless"))] -compile_error!("Only one of `sp1` and `stateless` can be enabled at a time."); - -// test-levm / test-sp1 read snobal-devnet-6 + legacy from `vectors/`. -// test-stateless reads zkevm@v0.6.2 (EIP-8025 canonical bundle) from a separate -// `vectors_zkevm/` so the bundles don't overlay each other. +// test-levm reads snobal-devnet-6 + legacy from `vectors/`. +// test-stateless reads the generated #3248+#3278 conformance vectors from a separate +// `vectors_stateless_3278/` so the bundles do not overlay each other. #[cfg(feature = "stateless")] const TEST_FOLDER: &str = "vectors_zkevm/"; #[cfg(not(feature = "stateless"))] @@ -27,18 +22,11 @@ const SKIPPED_BASE: &[&str] = &[ ]; // Extra skips added only for prover backends. -#[cfg(all(feature = "sp1", not(feature = "stateless")))] -const EXTRA_SKIPS: &[&str] = &[ - // I believe these tests fail because of how much stress they put into the zkVM, they probably cause an OOM though this should be checked - "static_Call50000", - "Return50000", - "static_Call1MB1024Calldepth", -]; // The stateless run executes the zkevm@v0.6.2 bundle (`vectors_zkevm/`), filled against // `tests-glamsterdam-devnet@v7.2.0` — the same base as the live `vectors/` fixtures on this // branch. v0.6.2 fixes the EIP-8282 fill (PR ethereum/execution-specs#3157): the canonical // `SszExecutionRequests` now carries the builder-deposit (0x03) and builder-exit (0x04) request -// lists, mirrored in `eip8025_ssz::ExecutionRequests`. The whole bundle re-executes cleanly, so +// lists, mirrored in `stateless_ssz::ExecutionRequests`. The whole bundle re-executes cleanly, so // no blanket skip and no per-fork skip are needed. Per-fixture leniency cases // (`*_extra_unused_*` padding, deliberately-invalid witnesses) are handled in `test_runner.rs`. // Amsterdam+ fixtures are skipped in the stateless run by fork (see @@ -48,16 +36,17 @@ const EXTRA_SKIPS: &[&str] = &[ // fork-based (not name-based), so no per-test entries are needed here. #[cfg(feature = "stateless")] const EXTRA_SKIPS: &[&str] = &[]; -#[cfg(not(any(feature = "sp1", feature = "stateless")))] +#[cfg(not(feature = "stateless"))] const EXTRA_SKIPS: &[&str] = &[]; -// Select backend +// Whether to run stateless validation after the stateful run. There is no backend +// choice any more: the in-memory paths call `validate_blocks_statelessly` and the +// wire path calls the guest entrypoint directly, so nothing dispatches on a +// prover backend. #[cfg(feature = "stateless")] -const BACKEND: Option = Some(BackendType::Exec); -#[cfg(all(feature = "sp1", not(feature = "stateless")))] -const BACKEND: Option = Some(BackendType::SP1); -#[cfg(not(any(feature = "sp1", feature = "stateless")))] -const BACKEND: Option = None; +const RUN_STATELESS: bool = true; +#[cfg(not(feature = "stateless"))] +const RUN_STATELESS: bool = false; fn blockchain_runner(path: &Path) -> datatest_stable::Result<()> { // Compose the final skip list @@ -67,7 +56,7 @@ fn blockchain_runner(path: &Path) -> datatest_stable::Result<()> { .chain(EXTRA_SKIPS.iter().copied()) .collect(); - parse_and_execute(path, Some(&skips), BACKEND) + parse_and_execute(path, Some(&skips), RUN_STATELESS) } datatest_stable::harness!(blockchain_runner, TEST_FOLDER, r".*"); From bf5a1971e3dc05ddd68732777ea5b214613cb803 Mon Sep 17 00:00:00 2001 From: Lucas Fiegl Date: Wed, 5 Aug 2026 10:08:09 -0300 Subject: [PATCH 14/30] feat(l1): port stateless-validator runner crate --- .../stateless-validator/Cargo.lock | 3700 +++++++++++++++++ .../stateless-validator/Cargo.toml | 81 + .../stateless-validator/src/crypto/mod.rs | 27 + .../stateless-validator/src/crypto/openvm.rs | 550 +++ .../src/crypto/zkvm_interface.rs | 356 ++ .../stateless-validator/src/lib.rs | 49 + .../stateless-validator/src/platform.rs | 95 + .../stateless-validator/tests/common/mod.rs | 105 + .../tests/host_fixtures.rs | 71 + .../tests/platform_parity.rs | 51 + 10 files changed, 5085 insertions(+) create mode 100644 crates/guest-program/stateless-validator/Cargo.lock create mode 100644 crates/guest-program/stateless-validator/Cargo.toml create mode 100644 crates/guest-program/stateless-validator/src/crypto/mod.rs create mode 100644 crates/guest-program/stateless-validator/src/crypto/openvm.rs create mode 100644 crates/guest-program/stateless-validator/src/crypto/zkvm_interface.rs create mode 100644 crates/guest-program/stateless-validator/src/lib.rs create mode 100644 crates/guest-program/stateless-validator/src/platform.rs create mode 100644 crates/guest-program/stateless-validator/tests/common/mod.rs create mode 100644 crates/guest-program/stateless-validator/tests/host_fixtures.rs create mode 100644 crates/guest-program/stateless-validator/tests/platform_parity.rs diff --git a/crates/guest-program/stateless-validator/Cargo.lock b/crates/guest-program/stateless-validator/Cargo.lock new file mode 100644 index 00000000000..774f2aba3d2 --- /dev/null +++ b/crates/guest-program/stateless-validator/Cargo.lock @@ -0,0 +1,3700 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addchain" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e33f6a175ec6a9e0aca777567f9ff7c3deefc255660df887e7fa3585e9801d8" +dependencies = [ + "num-bigint 0.3.3", + "num-integer", + "num-traits", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "ark-bn254" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d69eab57e8d2663efa5c63135b2af4f396d66424f88954c21104125ab6b3e6bc" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-std", +] + +[[package]] +name = "ark-ec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" +dependencies = [ + "ahash", + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "educe", + "fnv", + "hashbrown 0.15.5", + "itertools 0.13.0", + "num-bigint 0.4.8", + "num-integer", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "arrayvec", + "digest", + "educe", + "itertools 0.13.0", + "num-bigint 0.4.8", + "num-traits", + "paste", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint 0.4.8", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-poly" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" +dependencies = [ + "ahash", + "ark-ff", + "ark-serialize", + "ark-std", + "educe", + "fnv", + "hashbrown 0.15.5", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-serialize-derive", + "ark-std", + "arrayvec", + "digest", + "num-bigint 0.4.8", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 1.3.0", + "syn 2.0.119", +] + +[[package]] +name = "bitcoin-consensus-encoding" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "207311705279250ba465076a1bac4b1ac982855fff73fc5f67e22158ac58cdc9" +dependencies = [ + "bitcoin-internals", + "hex-conservative 1.2.0", + "serde", +] + +[[package]] +name = "bitcoin-internals" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d573f4cf32996a8dce612e4348cece65a241f1882ed594047c9ba348e8869fa5" + +[[package]] +name = "bitcoin-io" +version = "0.1.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb5de036369d1ac59d3c1819ebc4d850f89466f5401c571a285b6ed564a4cb78" +dependencies = [ + "bitcoin-consensus-encoding", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2" +dependencies = [ + "bitcoin-io", + "hex-conservative 0.2.2", +] + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2b_simd" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79834656f71332577234b50bfc009996f7449e0c056884e6a02492ded0ca2f3" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bls12_381" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bc6d6292be3a19e6379786dac800f551e5865a5bb51ebbe3064ab80433f403" +dependencies = [ + "ff", + "group", + "pairing", + "rand_core", + "subtle", +] + +[[package]] +name = "bls12_381" +version = "0.8.0" +source = "git+https://github.com/zkcrypto/bls12_381?rev=6bb96951d5c2035caf4989b6e4a018435379590f#6bb96951d5c2035caf4989b6e4a018435379590f" +dependencies = [ + "digest", + "ff", + "group", + "rand_core", + "subtle", +] + +[[package]] +name = "blst" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20659f9bbee16cbbd2f7393e40ab6309f5a98f76a2eb57a995ec508b72387fe" +dependencies = [ + "cc", + "glob", + "threadpool", + "zeroize", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + +[[package]] +name = "bytecheck" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26333eeac754f0ad8a6bcd0eb0ac012156302e4e16b852b72ee399aea4f12c29" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "rancor", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46d07918caa9eeaaf06b7873925c53a61daac173539b4f7715090745e44e4e69" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "shlex 2.0.1", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "clang-sys" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const_format" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" +dependencies = [ + "const_format_proc_macros", + "konst", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn 2.0.119", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "elf" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4445909572dbd556c457c849c4ca58623d84b27c8fff1e74b0b4227d8b90d17b" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "enum-ordinalize" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89dd01549b09589510cf0647475075d12071456586d70f5c75c98ae2a5537677" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65863d15a4ce2888bd2f0f543cc963d3879c3a022c8ee43f6141d479a3ac815" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "ere-platform-core" +version = "0.13.0" +source = "git+https://github.com/eth-act/ere?rev=a25f1aed9664c3b63e73ef05360090a4c41da31b#a25f1aed9664c3b63e73ef05360090a4c41da31b" + +[[package]] +name = "ethbloom" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c321610643004cf908ec0f5f2aa0d8f1f8e14b540562a2887a1111ff1ecbf7b" +dependencies = [ + "crunchy", + "fixed-hash", + "impl-rlp", + "impl-serde", + "tiny-keccak", +] + +[[package]] +name = "ethereum-types" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab15ed80916029f878e0267c3a9f92b67df55e79af370bf66199059ae2b4ee3" +dependencies = [ + "ethbloom", + "fixed-hash", + "impl-rlp", + "impl-serde", + "primitive-types", + "uint", +] + +[[package]] +name = "ethrex-common" +version = "23.0.0" +dependencies = [ + "bytes", + "crc32fast", + "ethereum-types", + "ethrex-crypto", + "ethrex-rlp", + "ethrex-trie", + "hex", + "hex-literal 0.4.1", + "hex-simd", + "indexmap 2.14.0", + "lazy_static", + "libc", + "libssz", + "libssz-derive", + "libssz-merkle", + "libssz-types", + "lru 0.16.4", + "once_cell", + "rkyv", + "rustc-hash", + "serde", + "serde_json", + "sha2", + "thiserror", + "tracing", +] + +[[package]] +name = "ethrex-crypto" +version = "23.0.0" +dependencies = [ + "ark-bn254", + "ark-ec", + "ark-ff", + "blst", + "ethereum-types", + "hex-literal 0.4.1", + "k256 0.13.4 (registry+https://github.com/rust-lang/crates.io-index)", + "kzg-rs", + "malachite", + "num-bigint 0.4.8", + "p256 0.13.2 (registry+https://github.com/rust-lang/crates.io-index)", + "ripemd", + "secp256k1", + "sha2", + "thiserror", + "tiny-keccak", +] + +[[package]] +name = "ethrex-guest-program" +version = "23.0.0" +dependencies = [ + "bytes", + "ethereum-types", + "ethrex-common", + "ethrex-crypto", + "ethrex-l2-common", + "ethrex-rlp", + "ethrex-vm", + "hex", + "libssz", + "libssz-derive", + "libssz-merkle", + "libssz-types", + "rkyv", + "serde", + "serde_with", + "thiserror", +] + +[[package]] +name = "ethrex-l2-common" +version = "23.0.0" +dependencies = [ + "bytes", + "ethereum-types", + "ethrex-common", + "ethrex-crypto", + "k256 0.13.4 (registry+https://github.com/rust-lang/crates.io-index)", + "lambdaworks-crypto", + "rkyv", + "serde", + "serde_with", + "thiserror", + "tracing", +] + +[[package]] +name = "ethrex-levm" +version = "23.0.0" +dependencies = [ + "bytes", + "derive_more", + "ethrex-common", + "ethrex-crypto", + "ethrex-rlp", + "libssz", + "malachite", + "rustc-hash", + "serde", + "strum", + "thiserror", +] + +[[package]] +name = "ethrex-rlp" +version = "23.0.0" +dependencies = [ + "bytes", + "ethereum-types", + "thiserror", +] + +[[package]] +name = "ethrex-stateless-validator" +version = "23.0.0" +dependencies = [ + "bls12_381 0.8.0 (git+https://github.com/zkcrypto/bls12_381?rev=6bb96951d5c2035caf4989b6e4a018435379590f)", + "ere-platform-core", + "ethrex-common", + "ethrex-crypto", + "ethrex-guest-program", + "hex", + "k256 0.13.4 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "libssz", + "libssz-merkle", + "libssz-types", + "openvm-curve-utils", + "openvm-ecc-guest 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-keccak256", + "openvm-kzg", + "openvm-pairing 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-sha2", + "p256 0.13.2 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "serde", + "serde_json", + "thiserror", + "zkvm-interface", +] + +[[package]] +name = "ethrex-trie" +version = "23.0.0" +dependencies = [ + "anyhow", + "bytes", + "crossbeam", + "ethereum-types", + "ethrex-crypto", + "ethrex-rlp", + "hashbrown 0.15.5", + "rayon", + "rkyv", + "rustc-hash", + "serde", + "spin 0.9.9", + "thiserror", +] + +[[package]] +name = "ethrex-vm" +version = "23.0.0" +dependencies = [ + "bytes", + "derive_more", + "dyn-clone", + "ethrex-common", + "ethrex-crypto", + "ethrex-levm", + "ethrex-rlp", + "rustc-hash", + "serde", + "thiserror", + "tracing", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "bitvec", + "byteorder", + "ff_derive", + "rand_core", + "subtle", +] + +[[package]] +name = "ff_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f10d12652036b0e99197587c6ba87a8fc3031986499973c030d8b44fcc151b60" +dependencies = [ + "addchain", + "num-bigint 0.3.3", + "num-integer", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rand", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-executor" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gcd" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d758ba1b47b00caf47f24925c0074ecb20d6dfcffe7f6d53395c0465674841a" + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "halo2curves-axiom" +version = "0.7.2" +source = "git+https://github.com/axiom-crypto/halo2curves.git?tag=v0.7.2#3a65a710e27fe03711f6fb4fc0c4469ae351974a" +dependencies = [ + "blake2b_simd", + "digest", + "ff", + "group", + "hex", + "lazy_static", + "num-bigint 0.4.8", + "num-traits", + "pairing", + "pasta_curves", + "paste", + "rand", + "rand_core", + "rayon", + "serde", + "serde_arrays 0.1.0", + "sha2", + "static_assertions", + "subtle", + "unroll", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex-conservative" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35431185f361ccf3ffc58254628af5f1f5d5f28531da2e02e5d6c82bbc282a10" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "hex-literal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" + +[[package]] +name = "hex-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f7685beb53fc20efc2605f32f5d51e9ba18b8ef237961d1760169d2290d3bee" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "impl-codec" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d40b9d5e17727407e55028eafc22b2dc68781786e6d7eb8a21103f5058e3a14" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-rlp" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54ed8ad1f3877f7e775b8cbf30ed1bd3209a95401817f19a0eb4402d13f8cf90" +dependencies = [ + "rlp", +] + +[[package]] +name = "impl-serde" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a143eada6a1ec4aefa5049037a26a6d597bfd64f8c026d07b77133e02b7dd0b" +dependencies = [ + "serde", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", + "signature", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "git+https://github.com/openvm-org/openvm.git?tag=v2.0.0#15a7ab6baed03d75050dbef2bbad4b4e98fb8dba" +dependencies = [ + "ecdsa", + "elliptic-curve", + "ff", + "hex-literal 1.1.0", + "num-bigint 0.4.8", + "openvm 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-algebra-guest 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-algebra-moduli-macros 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-ecc-guest 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-ecc-sw-macros 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "serde", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + +[[package]] +name = "kzg-rs" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8b4f55c3dedcfaa8668de1dfc8469e7a32d441c28edf225ed1f566fb32977d" +dependencies = [ + "ff", + "hex", + "serde_arrays 0.2.0", + "sha2", + "sp1_bls12_381", + "spin 0.9.9", +] + +[[package]] +name = "lambdaworks-crypto" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58b1a1c1102a5a7fbbda117b79fb3a01e033459c738a3c1642269603484fd1c1" +dependencies = [ + "lambdaworks-math", + "rand", + "rand_chacha", + "serde", + "sha2", + "sha3", +] + +[[package]] +name = "lambdaworks-math" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "018a95aa873eb49896a858dee0d925c33f3978d073c64b08dd4f2c9b35a017c6" +dependencies = [ + "getrandom", + "num-bigint 0.4.8", + "num-traits", + "rand", + "serde", + "serde_json", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin 0.9.9", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libssz" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d498c0482bba87d2647ea4601ea76cf2b498065e3958798a88f49274f3ced5e9" +dependencies = [ + "smallvec", +] + +[[package]] +name = "libssz-derive" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ddfb5c969c28a4a54043e630f80c723352637bd1020f256ee3ac7a8814922b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "libssz-merkle" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63c6d6d5ce5d79bba66bc98c99869eedffedf7f14f0aa0915f1a62802650bdf6" +dependencies = [ + "libssz", + "sha2", +] + +[[package]] +name = "libssz-types" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "747273ab2d923e82ed147091fe0fb3e602dd2012c872cdad5efe69e27c3b4099" +dependencies = [ + "libssz", + "libssz-merkle", + "smallvec", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "malachite" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec410515e231332b14cd986a475d1c3323bcfa4c7efc038bfa1d5b410b1c57e4" +dependencies = [ + "malachite-base", + "malachite-nz", + "malachite-q", +] + +[[package]] +name = "malachite-base" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c738d3789301e957a8f7519318fcbb1b92bb95863b28f6938ae5a05be6259f34" +dependencies = [ + "hashbrown 0.15.5", + "itertools 0.14.0", + "libm", + "ryu", +] + +[[package]] +name = "malachite-nz" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1707c9a1fa36ce21749b35972bfad17bbf34cf5a7c96897c0491da321e387d3b" +dependencies = [ + "itertools 0.14.0", + "libm", + "malachite-base", + "wide", +] + +[[package]] +name = "malachite-q" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d764801aa4e96bbb69b389dcd03b50075345131cd63ca2e380bca71cc37a3675" +dependencies = [ + "itertools 0.14.0", + "malachite-base", + "malachite-nz", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "munge" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c" +dependencies = [ + "munge_macro", +] + +[[package]] +name = "munge_macro" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6f7833f2cbf2360a6cfd58cd41a53aa7a90bd4c202f5b1c7dd2ed73c57b2c3" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", + "rand", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-modular" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a5fe11d4135c3bcdf3a95b18b194afa9608a5f6ff034f5d857bc9a27fb0119" +dependencies = [ + "num-bigint 0.4.8", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-prime" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e238432a7881ec7164503ccc516c014bf009be7984cde1ba56837862543bdec3" +dependencies = [ + "bitvec", + "either", + "lru 0.12.5", + "num-bigint 0.4.8", + "num-integer", + "num-modular", + "num-traits", + "rand", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openvm" +version = "2.0.0" +source = "git+https://github.com/openvm-org/openvm.git?tag=v2.0.0#15a7ab6baed03d75050dbef2bbad4b4e98fb8dba" +dependencies = [ + "bytemuck", + "num-bigint 0.4.8", + "openvm-custom-insn 0.1.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-platform 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-rv32im-guest 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "serde", +] + +[[package]] +name = "openvm" +version = "2.0.0" +source = "git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3#3ee99e5002e123a6ca50c815c3e40ab0bd96b0e3" +dependencies = [ + "bytemuck", + "num-bigint 0.4.8", + "openvm-custom-insn 0.1.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "openvm-platform 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "openvm-rv32im-guest 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "serde", +] + +[[package]] +name = "openvm-algebra-complex-macros" +version = "2.0.0" +source = "git+https://github.com/openvm-org/openvm.git?tag=v2.0.0#15a7ab6baed03d75050dbef2bbad4b4e98fb8dba" +dependencies = [ + "openvm-macros-common 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "openvm-algebra-complex-macros" +version = "2.0.0" +source = "git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3#3ee99e5002e123a6ca50c815c3e40ab0bd96b0e3" +dependencies = [ + "openvm-macros-common 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "openvm-algebra-guest" +version = "2.0.0" +source = "git+https://github.com/openvm-org/openvm.git?tag=v2.0.0#15a7ab6baed03d75050dbef2bbad4b4e98fb8dba" +dependencies = [ + "halo2curves-axiom", + "num-bigint 0.4.8", + "once_cell", + "openvm-algebra-complex-macros 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-algebra-moduli-macros 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-custom-insn 0.1.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-rv32im-guest 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "serde-big-array", + "strum_macros 0.26.4", +] + +[[package]] +name = "openvm-algebra-guest" +version = "2.0.0" +source = "git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3#3ee99e5002e123a6ca50c815c3e40ab0bd96b0e3" +dependencies = [ + "halo2curves-axiom", + "num-bigint 0.4.8", + "once_cell", + "openvm-algebra-complex-macros 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "openvm-algebra-moduli-macros 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "openvm-custom-insn 0.1.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "openvm-rv32im-guest 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "serde-big-array", + "strum_macros 0.26.4", +] + +[[package]] +name = "openvm-algebra-moduli-macros" +version = "2.0.0" +source = "git+https://github.com/openvm-org/openvm.git?tag=v2.0.0#15a7ab6baed03d75050dbef2bbad4b4e98fb8dba" +dependencies = [ + "num-bigint 0.4.8", + "num-prime", + "openvm-macros-common 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "openvm-algebra-moduli-macros" +version = "2.0.0" +source = "git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3#3ee99e5002e123a6ca50c815c3e40ab0bd96b0e3" +dependencies = [ + "num-bigint 0.4.8", + "num-prime", + "openvm-macros-common 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "openvm-curve-utils" +version = "0.4.0" +source = "git+https://github.com/axiom-crypto/openvm-eth?rev=aa8bbe17e624d317f14ea925df7c256c0b205134#aa8bbe17e624d317f14ea925df7c256c0b205134" +dependencies = [ + "hex-literal 1.1.0", + "openvm-ecc-guest 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "openvm-pairing 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", +] + +[[package]] +name = "openvm-custom-insn" +version = "0.1.0" +source = "git+https://github.com/openvm-org/openvm.git?tag=v2.0.0#15a7ab6baed03d75050dbef2bbad4b4e98fb8dba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "openvm-custom-insn" +version = "0.1.0" +source = "git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3#3ee99e5002e123a6ca50c815c3e40ab0bd96b0e3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "openvm-ecc-guest" +version = "2.0.0" +source = "git+https://github.com/openvm-org/openvm.git?tag=v2.0.0#15a7ab6baed03d75050dbef2bbad4b4e98fb8dba" +dependencies = [ + "ecdsa", + "elliptic-curve", + "group", + "halo2curves-axiom", + "once_cell", + "openvm 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-algebra-guest 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-custom-insn 0.1.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-ecc-sw-macros 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-rv32im-guest 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "serde", + "strum_macros 0.26.4", +] + +[[package]] +name = "openvm-ecc-guest" +version = "2.0.0" +source = "git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3#3ee99e5002e123a6ca50c815c3e40ab0bd96b0e3" +dependencies = [ + "ecdsa", + "elliptic-curve", + "group", + "halo2curves-axiom", + "once_cell", + "openvm 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "openvm-algebra-guest 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "openvm-custom-insn 0.1.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "openvm-ecc-sw-macros 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "openvm-rv32im-guest 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "serde", + "strum_macros 0.26.4", +] + +[[package]] +name = "openvm-ecc-sw-macros" +version = "2.0.0" +source = "git+https://github.com/openvm-org/openvm.git?tag=v2.0.0#15a7ab6baed03d75050dbef2bbad4b4e98fb8dba" +dependencies = [ + "openvm-macros-common 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "openvm-ecc-sw-macros" +version = "2.0.0" +source = "git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3#3ee99e5002e123a6ca50c815c3e40ab0bd96b0e3" +dependencies = [ + "openvm-macros-common 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "openvm-keccak256" +version = "2.0.0" +source = "git+https://github.com/openvm-org/openvm.git?tag=v2.0.0#15a7ab6baed03d75050dbef2bbad4b4e98fb8dba" +dependencies = [ + "openvm-keccak256-guest", + "spin 0.10.1", + "tiny-keccak", +] + +[[package]] +name = "openvm-keccak256-guest" +version = "2.0.0" +source = "git+https://github.com/openvm-org/openvm.git?tag=v2.0.0#15a7ab6baed03d75050dbef2bbad4b4e98fb8dba" +dependencies = [ + "openvm-platform 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", +] + +[[package]] +name = "openvm-kzg" +version = "0.4.0" +source = "git+https://github.com/axiom-crypto/openvm-eth?rev=aa8bbe17e624d317f14ea925df7c256c0b205134#aa8bbe17e624d317f14ea925df7c256c0b205134" +dependencies = [ + "bls12_381 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "hex", + "hex-literal 1.1.0", + "openvm-algebra-guest 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "openvm-curve-utils", + "openvm-ecc-guest 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "openvm-pairing 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "serde", + "serde-big-array", + "spin 0.10.1", +] + +[[package]] +name = "openvm-macros-common" +version = "2.0.0" +source = "git+https://github.com/openvm-org/openvm.git?tag=v2.0.0#15a7ab6baed03d75050dbef2bbad4b4e98fb8dba" +dependencies = [ + "syn 2.0.119", +] + +[[package]] +name = "openvm-macros-common" +version = "2.0.0" +source = "git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3#3ee99e5002e123a6ca50c815c3e40ab0bd96b0e3" +dependencies = [ + "syn 2.0.119", +] + +[[package]] +name = "openvm-pairing" +version = "2.0.0" +source = "git+https://github.com/openvm-org/openvm.git?tag=v2.0.0#15a7ab6baed03d75050dbef2bbad4b4e98fb8dba" +dependencies = [ + "group", + "hex-literal 1.1.0", + "itertools 0.14.0", + "num-bigint 0.4.8", + "num-traits", + "openvm 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-algebra-complex-macros 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-algebra-guest 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-algebra-moduli-macros 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-custom-insn 0.1.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-ecc-guest 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-ecc-sw-macros 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-pairing-guest 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-platform 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-rv32im-guest 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "serde", +] + +[[package]] +name = "openvm-pairing" +version = "2.0.0" +source = "git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3#3ee99e5002e123a6ca50c815c3e40ab0bd96b0e3" +dependencies = [ + "group", + "hex-literal 1.1.0", + "itertools 0.14.0", + "num-bigint 0.4.8", + "num-traits", + "openvm 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "openvm-algebra-complex-macros 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "openvm-algebra-guest 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "openvm-algebra-moduli-macros 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "openvm-custom-insn 0.1.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "openvm-ecc-guest 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "openvm-ecc-sw-macros 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "openvm-pairing-guest 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "openvm-platform 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "openvm-rv32im-guest 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "serde", +] + +[[package]] +name = "openvm-pairing-guest" +version = "2.0.0" +source = "git+https://github.com/openvm-org/openvm.git?tag=v2.0.0#15a7ab6baed03d75050dbef2bbad4b4e98fb8dba" +dependencies = [ + "hex-literal 1.1.0", + "itertools 0.14.0", + "lazy_static", + "num-bigint 0.4.8", + "num-traits", + "openvm 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-algebra-guest 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-algebra-moduli-macros 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-custom-insn 0.1.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-ecc-guest 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "serde", + "strum_macros 0.26.4", +] + +[[package]] +name = "openvm-pairing-guest" +version = "2.0.0" +source = "git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3#3ee99e5002e123a6ca50c815c3e40ab0bd96b0e3" +dependencies = [ + "hex-literal 1.1.0", + "itertools 0.14.0", + "lazy_static", + "num-bigint 0.4.8", + "num-traits", + "openvm 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "openvm-algebra-guest 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "openvm-algebra-moduli-macros 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "openvm-custom-insn 0.1.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "openvm-ecc-guest 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "serde", + "strum_macros 0.26.4", +] + +[[package]] +name = "openvm-platform" +version = "2.0.0" +source = "git+https://github.com/openvm-org/openvm.git?tag=v2.0.0#15a7ab6baed03d75050dbef2bbad4b4e98fb8dba" +dependencies = [ + "libm", + "openvm-custom-insn 0.1.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-rv32im-guest 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", +] + +[[package]] +name = "openvm-platform" +version = "2.0.0" +source = "git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3#3ee99e5002e123a6ca50c815c3e40ab0bd96b0e3" +dependencies = [ + "libm", + "openvm-custom-insn 0.1.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "openvm-rv32im-guest 2.0.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", +] + +[[package]] +name = "openvm-rv32im-guest" +version = "2.0.0" +source = "git+https://github.com/openvm-org/openvm.git?tag=v2.0.0#15a7ab6baed03d75050dbef2bbad4b4e98fb8dba" +dependencies = [ + "openvm-custom-insn 0.1.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "strum_macros 0.26.4", +] + +[[package]] +name = "openvm-rv32im-guest" +version = "2.0.0" +source = "git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3#3ee99e5002e123a6ca50c815c3e40ab0bd96b0e3" +dependencies = [ + "openvm-custom-insn 0.1.0 (git+https://github.com/openvm-org/openvm.git?branch=develop-v2.0.0-rc.3)", + "strum_macros 0.26.4", +] + +[[package]] +name = "openvm-sha2" +version = "2.0.0" +source = "git+https://github.com/openvm-org/openvm.git?tag=v2.0.0#15a7ab6baed03d75050dbef2bbad4b4e98fb8dba" +dependencies = [ + "openvm-sha2-guest", + "sha2", +] + +[[package]] +name = "openvm-sha2-guest" +version = "2.0.0" +source = "git+https://github.com/openvm-org/openvm.git?tag=v2.0.0#15a7ab6baed03d75050dbef2bbad4b4e98fb8dba" +dependencies = [ + "openvm-platform 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", +] + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p256" +version = "0.13.2" +source = "git+https://github.com/openvm-org/openvm.git?tag=v2.0.0#15a7ab6baed03d75050dbef2bbad4b4e98fb8dba" +dependencies = [ + "ecdsa", + "elliptic-curve", + "ff", + "hex-literal 1.1.0", + "num-bigint 0.4.8", + "openvm 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-algebra-guest 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-algebra-moduli-macros 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-ecc-guest 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "openvm-ecc-sw-macros 2.0.0 (git+https://github.com/openvm-org/openvm.git?tag=v2.0.0)", + "serde", +] + +[[package]] +name = "p3-bn254-fr" +version = "0.4.3-succinct" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2077757c7cb514202ccb5368f521f23f5709c720599e6545c683c66e0a52d2d8" +dependencies = [ + "ff", + "num-bigint 0.4.8", + "p3-field", + "p3-poseidon2", + "p3-symmetric", + "rand", + "serde", +] + +[[package]] +name = "p3-challenger" +version = "0.4.3-succinct" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6a908924d43e4cfb93fb41c8346cac211b70314385a9037e9241f5b7f3eaf77" +dependencies = [ + "p3-field", + "p3-maybe-rayon", + "p3-symmetric", + "p3-util", + "serde", + "tracing", +] + +[[package]] +name = "p3-dft" +version = "0.4.3-succinct" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be6408b10a2c27eb13a7d5580c546c2179a8dc7dbc10a990657311891f9b41c0" +dependencies = [ + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-util", + "tracing", +] + +[[package]] +name = "p3-field" +version = "0.4.3-succinct" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dc75969ca3ac847f43e632ab979d59ff7a68f9eac8dbf8edcbba47fc2e1d3aa" +dependencies = [ + "itertools 0.12.1", + "num-bigint 0.4.8", + "num-traits", + "p3-util", + "rand", + "serde", +] + +[[package]] +name = "p3-koala-bear" +version = "0.4.3-succinct" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a9683cd0ef68100df7c62490533047bcf19c04c4a0fa1efc9d7c1e03e31f6b3" +dependencies = [ + "cfg-if", + "num-bigint 0.4.8", + "p3-field", + "p3-mds", + "p3-poseidon2", + "p3-symmetric", + "rand", + "rustc_version", + "serde", +] + +[[package]] +name = "p3-matrix" +version = "0.4.3-succinct" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c3f150ceb90e09539413bf481e618d05ee19210b4e467d2902eb82d2e15281" +dependencies = [ + "itertools 0.12.1", + "p3-field", + "p3-maybe-rayon", + "p3-util", + "rand", + "serde", + "tracing", +] + +[[package]] +name = "p3-maybe-rayon" +version = "0.4.3-succinct" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0641952b42da45e1dfa2d4a2a3163e330f944ad9740942f35026c0a71a605f1" + +[[package]] +name = "p3-mds" +version = "0.4.3-succinct" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4a5f250e174dcfca5cbeac6ad75713924e7e7320e0a335e3c50b8b1f4fe8ec" +dependencies = [ + "itertools 0.12.1", + "p3-dft", + "p3-field", + "p3-matrix", + "p3-symmetric", + "p3-util", + "rand", +] + +[[package]] +name = "p3-poseidon2" +version = "0.4.3-succinct" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "522986377b2164c5f94f2dae88e0e0a3d169cc6239202ef4aeb4322d60feffd0" +dependencies = [ + "gcd", + "p3-field", + "p3-mds", + "p3-symmetric", + "rand", + "serde", +] + +[[package]] +name = "p3-symmetric" +version = "0.4.3-succinct" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9047ce85c086a9b3f118e10078f10636f7bfeed5da871a04da0b61400af8793a" +dependencies = [ + "itertools 0.12.1", + "p3-field", + "serde", +] + +[[package]] +name = "p3-util" +version = "0.4.3-succinct" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff962f8eaa5f36e0447cee7c241f6b4b475fadf3ee61f154327a26bb4e009ba" +dependencies = [ + "serde", +] + +[[package]] +name = "pairing" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" +dependencies = [ + "group", +] + +[[package]] +name = "parity-scale-codec" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "const_format", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "rustversion", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pasta_curves" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3437083215c505e867eea5478371feba43d7689d6d15ec0a209eb46fb0d4cda6" +dependencies = [ + "blake2b_simd", + "ff", + "group", + "lazy_static", + "rand", + "static_assertions", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "primitive-types" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15600a7d856470b7d278b3fe0e311fe28c2526348549f8ef2ff7db3299c87f5" +dependencies = [ + "fixed-hash", + "impl-codec", + "impl-rlp", + "impl-serde", + "uint", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "ptr_meta" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743da816b98c921cdbe8628ef7381b76f25ecf4da599fc80aca90eae7ef70cc0" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c8d9ca532f185d5d4db7a7c9d51420b452168ea1c2b913953281bd6fe1fcbd0" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rancor" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b534442d0fcdb55d66f373d9cac6d33b6293a2335bc2136dbd06ce0e87d2572" +dependencies = [ + "ptr_meta", +] + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "ref-cast" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rend" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "663ba70707f96e871406fe10d68128412e619b06d1d47cb91c3a4c6501176240" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest", +] + +[[package]] +name = "rkyv" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9776093b7ca170454ab1406954f7b7d97a57c51dc6c0642957fb2ef25c2d399" +dependencies = [ + "bytecheck", + "bytes", + "hashbrown 0.17.1", + "indexmap 2.14.0", + "munge", + "ptr_meta", + "rancor", + "rend", + "rkyv_derive", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c25ef604ac7dd839d44d64648952ea23c97866f124ff671b0ed2cf3ad9bb06e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "rlp" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa24e92bb2a83198bb76d661a71df9f7076b8c420b8696e4d3d97d50d94479e3" +dependencies = [ + "bytes", + "rustc-hex", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "safe_arch" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "secp256k1" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" +dependencies = [ + "bitcoin_hashes", + "rand", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-big-array" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_arrays" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38636132857f68ec3d5f3eb121166d2af33cb55174c4d5ff645db6165cbef0fd" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_arrays" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94a16b99c5ea4fe3daccd14853ad260ec00ea043b2708d1fd1da3106dcd8d9df" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slop-algebra" +version = "6.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c112cafd4c5c374d267a48c8976d12fca3b0f2cc2e44e6fb1343808310b4fc6" +dependencies = [ + "itertools 0.14.0", + "p3-field", + "serde", +] + +[[package]] +name = "slop-bn254" +version = "6.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea991a652ea2e55f0523649f5c9efbfb32cf9fdcc2bf3b3580b4343a94379503" +dependencies = [ + "ff", + "p3-bn254-fr", + "serde", + "slop-algebra", + "slop-challenger", + "slop-poseidon2", + "slop-symmetric", +] + +[[package]] +name = "slop-challenger" +version = "6.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b33f1eaadcd4159157758b0edcf1c14f470915f85c648e660e4740ff83e921" +dependencies = [ + "futures", + "p3-challenger", + "serde", + "slop-algebra", + "slop-symmetric", +] + +[[package]] +name = "slop-koala-bear" +version = "6.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8795ee9a92ecf0a604c0e3ed20e80bbc38772310c0622f94a2e1b66ca2455cb8" +dependencies = [ + "lazy_static", + "p3-koala-bear", + "serde", + "slop-algebra", + "slop-challenger", + "slop-poseidon2", + "slop-symmetric", +] + +[[package]] +name = "slop-poseidon2" +version = "6.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25561d9ecf4bcf1aa2e800560ac557bff6dad943b9dc5cd0e8427fceca1e8c65" +dependencies = [ + "p3-poseidon2", +] + +[[package]] +name = "slop-primitives" +version = "6.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b24ad70b49f40d6acfe7b1ccf042db25820abe305a4c2232f15204f63f0227" +dependencies = [ + "slop-algebra", +] + +[[package]] +name = "slop-symmetric" +version = "6.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e580d03bb5383aca1c12579a8a24b838afb12eb356439e740cba34904b6864" +dependencies = [ + "p3-symmetric", +] + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "sp1-lib" +version = "6.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "629b673d6aa3c666b41e14d699323413fa90195c906365ba8c49d1b8e4531151" +dependencies = [ + "bincode", + "serde", + "sp1-primitives", +] + +[[package]] +name = "sp1-primitives" +version = "6.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "421b4590a89bf7c263f8a64844a4034ee8cb73c351498911ce0acd18da6b5ed2" +dependencies = [ + "bincode", + "blake3", + "elf", + "hex", + "itertools 0.14.0", + "lazy_static", + "num-bigint 0.4.8", + "serde", + "sha2", + "slop-algebra", + "slop-bn254", + "slop-challenger", + "slop-koala-bear", + "slop-poseidon2", + "slop-primitives", + "slop-symmetric", +] + +[[package]] +name = "sp1_bls12_381" +version = "0.8.0-sp1-6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f23e41cd36168cc2e51e5d3e35ff0c34b204d945769a65591a76286d04b51e43" +dependencies = [ + "cfg-if", + "ff", + "group", + "pairing", + "rand_core", + "sp1-lib", + "subtle", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" + +[[package]] +name = "spin" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.119", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uint" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909988d098b2f738727b161a106cfc7cab00c539c2687a8836f8e565976fb53e" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unroll" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ad948c1cb799b1a70f836077721a92a35ac177d4daddf4c20a633786d4cf618" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wide" +version = "0.7.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" +dependencies = [ + "bytemuck", + "safe_arch", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zkvm-interface" +version = "0.1.0" +source = "git+https://github.com/eth-act/zkvm-standards?rev=282cd356c3a0498416bb0619f9c8a347ce9933fb#282cd356c3a0498416bb0619f9c8a347ce9933fb" +dependencies = [ + "bindgen", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/crates/guest-program/stateless-validator/Cargo.toml b/crates/guest-program/stateless-validator/Cargo.toml new file mode 100644 index 00000000000..353b70ccd05 --- /dev/null +++ b/crates/guest-program/stateless-validator/Cargo.toml @@ -0,0 +1,81 @@ +[package] +version = "23.0.0" +name = "ethrex-stateless-validator" +edition = "2024" +license = "MIT OR Apache-2.0" + +# Own workspace, like the guest bins. This crate pins zkVM SDK versions to what +# eth-act's `ere` catalog expects, which is deliberately independent of the L2 +# prover's pins — bumping SP1 here must not move the L2 verification keys. +[workspace] + +[dependencies] +thiserror = "2.0.9" + +ethrex-common = { path = "../../common/", default-features = false } +ethrex-crypto = { path = "../../common/crypto", default-features = false } +ethrex-guest-program = { path = "../", default-features = false } + +# SSZ encode/decode and merkleization. The mirror spike reached these through +# eth-act's `stateless-validator-common`; ethrex owns the wire types natively, so +# they are declared here directly. Versions match the root workspace. +libssz = "0.2.2" +libssz-merkle = "0.2.2" +libssz-types = "0.2.2" + +# ere platform abstraction (entrypoint / IO / cycle scopes) used by the per-zkVM +# bins. Pinned to one ere rev; the ere-compiler and ere-server image tags in +# tag_release.yaml are derived from the same rev. +ere-platform-core = { git = "https://github.com/eth-act/ere", rev = "a25f1aed9664c3b63e73ef05360090a4c41da31b", optional = true } + +# zkvm-standards syscall bindings backing the ZisK and SP1 crypto provider. This +# is what keeps guest crypto independent of per-SDK patched-crate stacks, so no +# sp1-patches tags are needed for the SP1 guest at all. +zkvm-interface = { git = "https://github.com/eth-act/zkvm-standards", rev = "282cd356c3a0498416bb0619f9c8a347ce9933fb", optional = true } + +# OpenVM crypto provider (OpenVM 2.0.0 line). Note openvm-kzg and +# openvm-curve-utils come from axiom-crypto/openvm-eth, NOT axiom-crypto/openvm-kzg. +openvm-ecc-guest = { git = "https://github.com/openvm-org/openvm.git", tag = "v2.0.0", optional = true } +openvm-sha2 = { git = "https://github.com/openvm-org/openvm.git", tag = "v2.0.0", optional = true } +openvm-pairing = { git = "https://github.com/openvm-org/openvm.git", tag = "v2.0.0", features = ["bn254", "bls12_381"], optional = true } +openvm-k256 = { git = "https://github.com/openvm-org/openvm.git", tag = "v2.0.0", package = "k256", optional = true } +openvm-p256 = { git = "https://github.com/openvm-org/openvm.git", tag = "v2.0.0", package = "p256", optional = true } +openvm-keccak256 = { git = "https://github.com/openvm-org/openvm.git", tag = "v2.0.0", optional = true } +openvm-curve-utils = { git = "https://github.com/axiom-crypto/openvm-eth", rev = "aa8bbe17e624d317f14ea925df7c256c0b205134", optional = true } +openvm-kzg = { git = "https://github.com/axiom-crypto/openvm-eth", rev = "aa8bbe17e624d317f14ea925df7c256c0b205134", optional = true } +bls12_381 = { git = "https://github.com/zkcrypto/bls12_381", rev = "6bb96951d5c2035caf4989b6e4a018435379590f", default-features = false, features = ["experimental"], optional = true } + +[dev-dependencies] +hex = "0.4.3" +serde = { version = "1.0.203", features = ["derive"] } +serde_json = "1.0.117" + +[features] +default = [] + +# Host-side execution (fixture tests, no zkVM): the native crypto backends +# `NativeCrypto` needs. +host = [ + "ethrex-crypto/blst", + "ethrex-crypto/kzg-rs", + "ethrex-crypto/secp256k1", +] + +# ere-platform entrypoint, used by the per-zkVM guest bins. +ere = ["dep:ere-platform-core"] + +# Crypto providers, one per zkVM. +zkvm-interface = ["dep:zkvm-interface"] +sp1 = ["zkvm-interface"] +zisk = ["zkvm-interface"] +openvm = [ + "dep:openvm-ecc-guest", + "dep:openvm-sha2", + "dep:openvm-pairing", + "dep:openvm-k256", + "dep:openvm-p256", + "dep:openvm-keccak256", + "dep:openvm-curve-utils", + "dep:openvm-kzg", + "dep:bls12_381", +] diff --git a/crates/guest-program/stateless-validator/src/crypto/mod.rs b/crates/guest-program/stateless-validator/src/crypto/mod.rs new file mode 100644 index 00000000000..c0335a3c476 --- /dev/null +++ b/crates/guest-program/stateless-validator/src/crypto/mod.rs @@ -0,0 +1,27 @@ +//! Crypto provider selection for the guest. +//! +//! Mirrors the ere-guests adapter modules: zisk/sp1 route through the +//! zkvm-standards `zkvm-interface` syscalls, openvm through its guest +//! libraries. This keeps guest crypto decoupled from per-SDK patched-crate +//! stacks (ere pins sp1 v6.3.1 / openvm v2.0.0, which the ethrex first-party +//! providers do not target). + +#[cfg(feature = "openvm")] +mod openvm; +#[cfg(feature = "zkvm-interface")] +mod zkvm_interface; + +use std::sync::Arc; + +use ethrex_crypto::Crypto; + +/// Returns the [`Crypto`] implementation for the active zkVM feature. +#[allow(unreachable_code)] +pub fn crypto() -> Arc { + #[cfg(feature = "openvm")] + return openvm::crypto(); + #[cfg(feature = "zkvm-interface")] + return zkvm_interface::crypto(); + #[cfg(not(any(feature = "openvm", feature = "zkvm-interface")))] + return Arc::new(ethrex_crypto::NativeCrypto); +} diff --git a/crates/guest-program/stateless-validator/src/crypto/openvm.rs b/crates/guest-program/stateless-validator/src/crypto/openvm.rs new file mode 100644 index 00000000000..db8f7b7a062 --- /dev/null +++ b/crates/guest-program/stateless-validator/src/crypto/openvm.rs @@ -0,0 +1,550 @@ +//! [`ethrex_crypto::Crypto`] implementation using OpenVM guest libraries. + +use alloc::{sync::Arc, vec, vec::Vec}; + +use bls12_381::hash_to_curve::MapToCurve; +use ethrex_crypto::{Crypto, CryptoError, NativeCrypto}; +use openvm_curve_utils::SubgroupCheck; +use openvm_ecc_guest::{ + AffinePoint, Group, + algebra::IntMod, + weierstrass::{IntrinsicCurve, WeierstrassPoint}, +}; +use openvm_k256::ecdsa::{RecoveryId, Signature, VerifyingKey}; +use openvm_keccak256::keccak256; +use openvm_kzg::{Bytes32, Bytes48, EnvKzgSettings, KzgProof}; +use openvm_p256::ecdsa::{ + Signature as P256Signature, VerifyingKey as P256VerifyingKey, + signature::hazmat::PrehashVerifier, +}; +use openvm_pairing::{ + PairingCheck, + bls12_381::{self as bls, Bls12_381}, + bn254::{self as bn, Bn254}, +}; +use openvm_sha2::{Digest, Sha256}; + +// BN254 constants +const BN_FQ_LEN: usize = 32; +const BN_G1_LEN: usize = 64; +const BN_G2_LEN: usize = 128; +/// BN_SCALAR_LEN specifies the number of bytes needed to represent an Fr element. +/// This is an element in the scalar field of BN254. +const BN_SCALAR_LEN: usize = 32; + +// BLS12-381 constants +const BLS_FP_LEN: usize = 48; +const BLS_G1_LEN: usize = 96; +const BLS_G2_LEN: usize = 192; + +/// Returns a [`Crypto`] implementation backed by OpenVM guest libraries. +#[inline] +pub(super) fn crypto() -> Arc { + Arc::new(OpenVmCrypto) +} + +#[derive(Debug, Default)] +struct OpenVmCrypto; + +impl Crypto for OpenVmCrypto { + #[inline] + fn secp256k1_ecrecover( + &self, + sig: &[u8; 64], + mut recid: u8, + msg: &[u8; 32], + ) -> Result<[u8; 32], CryptoError> { + let mut signature = + Signature::from_slice(sig).map_err(|_| CryptoError::InvalidSignature)?; + + if let Some(signature_normalized) = signature.normalize_s() { + signature = signature_normalized; + recid ^= 1; + } + + let recovery_id = RecoveryId::from_byte(recid).ok_or(CryptoError::InvalidRecoveryId)?; + + let recovered_key = + VerifyingKey::recover_from_prehash_noverify(msg, &signature.to_bytes(), recovery_id) + .map_err(|_| CryptoError::RecoveryFailed)?; + + // Hash the uncompressed SEC1 key without the 0x04 prefix. + let public_key = recovered_key.to_encoded_point(false); + Ok(keccak256(&public_key.as_bytes()[1..])) + } + + #[inline] + fn keccak256(&self, input: &[u8]) -> [u8; 32] { + keccak256(input) + } + + #[inline] + fn sha256(&self, input: &[u8]) -> [u8; 32] { + Sha256::digest(input).into() + } + + #[inline] + fn bn254_g1_add(&self, p1: &[u8], p2: &[u8]) -> Result<[u8; 64], CryptoError> { + let p1 = read_bn_g1_point(p1)?; + let p2 = read_bn_g1_point(p2)?; + Ok(encode_bn_g1_point(p1 + p2)) + } + + #[inline] + fn bn254_g1_mul(&self, point: &[u8], scalar: &[u8]) -> Result<[u8; 64], CryptoError> { + let point = read_bn_g1_point(point)?; + let scalar = read_bn_scalar(scalar)?; + Ok(encode_bn_g1_point(Bn254::msm(&[scalar], &[point]))) + } + + #[inline] + fn bn254_pairing_check(&self, pairs: &[(&[u8], &[u8])]) -> Result { + if pairs.is_empty() { + return Ok(true); + } + + let mut g1_points = Vec::with_capacity(pairs.len()); + let mut g2_points = Vec::with_capacity(pairs.len()); + for (g1_bytes, g2_bytes) in pairs { + let (g1_x, g1_y) = read_bn_g1_point(g1_bytes)?.into_coords(); + let (g2_x, g2_y) = read_bn_g2_point(g2_bytes)?.into_coords(); + g1_points.push(AffinePoint::new(g1_x, g1_y)); + g2_points.push(AffinePoint::new(g2_x, g2_y)); + } + + Ok(Bn254::pairing_check(&g1_points, &g2_points).is_ok()) + } + + #[inline] + fn modexp(&self, base: &[u8], exp: &[u8], modulus: &[u8]) -> Result, CryptoError> { + if is_bn254_fr(modulus) { + return Ok(accelerated_modexp_bn254_fr(base, exp)); + } + NativeCrypto.modexp(base, exp, modulus) + } + + #[inline] + fn secp256r1_verify(&self, msg: &[u8; 32], sig: &[u8; 64], pk: &[u8; 64]) -> bool { + // `from_slice` rejects zero and non-canonical r/s scalars. + let Ok(signature) = P256Signature::from_slice(sig) else { + return false; + }; + + let x_bytes: &[u8; 32] = match pk[..32].try_into() { + Ok(b) => b, + Err(_) => return false, + }; + let y_bytes: &[u8; 32] = match pk[32..].try_into() { + Ok(b) => b, + Err(_) => return false, + }; + let encoded_point = openvm_p256::EncodedPoint::from_affine_coordinates( + x_bytes.into(), + y_bytes.into(), + false, + ); + let Ok(verifying_key) = P256VerifyingKey::from_encoded_point(&encoded_point) else { + return false; + }; + + verifying_key.verify_prehash(msg, &signature).is_ok() + } + + #[inline] + fn verify_kzg_proof( + &self, + z: &[u8; 32], + y: &[u8; 32], + commitment: &[u8; 48], + proof: &[u8; 48], + ) -> Result<(), CryptoError> { + let env = EnvKzgSettings::default(); + let kzg_settings = env.get(); + + let commitment_bytes = Bytes48::from_slice(commitment) + .map_err(|_| CryptoError::InvalidInput("invalid commitment bytes"))?; + let z_bytes = + Bytes32::from_slice(z).map_err(|_| CryptoError::InvalidInput("invalid z bytes"))?; + let y_bytes = + Bytes32::from_slice(y).map_err(|_| CryptoError::InvalidInput("invalid y bytes"))?; + let proof_bytes = Bytes48::from_slice(proof) + .map_err(|_| CryptoError::InvalidInput("invalid proof bytes"))?; + + let valid = KzgProof::verify_kzg_proof( + &commitment_bytes, + &z_bytes, + &y_bytes, + &proof_bytes, + kzg_settings, + ) + .map_err(|_| CryptoError::VerificationFailed)?; + if valid { + Ok(()) + } else { + Err(CryptoError::VerificationFailed) + } + } + + #[inline] + fn bls12_381_g1_add( + &self, + a: ([u8; 48], [u8; 48]), + b: ([u8; 48], [u8; 48]), + ) -> Result<[u8; 96], CryptoError> { + // EIP-2537 G1ADD validates on-curve only, not subgroup membership. + let p1 = read_bls_g1_point_no_subgroup_check(&a)?; + let p2 = read_bls_g1_point_no_subgroup_check(&b)?; + Ok(encode_bls_g1_point(&(p1 + p2))) + } + + #[inline] + fn bls12_381_g1_msm( + &self, + pairs: &[(([u8; 48], [u8; 48]), [u8; 32])], + ) -> Result<[u8; 96], CryptoError> { + let mut points = Vec::with_capacity(pairs.len()); + let mut scalars = Vec::with_capacity(pairs.len()); + for (point, scalar) in pairs { + points.push(read_bls_g1_point(point)?); + scalars.push(read_bls_scalar(scalar)); + } + + if points.is_empty() { + return Ok([0u8; BLS_G1_LEN]); + } + + Ok(encode_bls_g1_point(&Bls12_381::msm(&scalars, &points))) + } + + #[inline] + fn bls12_381_g2_add( + &self, + a: ([u8; 48], [u8; 48], [u8; 48], [u8; 48]), + b: ([u8; 48], [u8; 48], [u8; 48], [u8; 48]), + ) -> Result<[u8; 192], CryptoError> { + // EIP-2537 G2ADD validates on-curve only, not subgroup membership. + let p1 = read_bls_g2_point_no_subgroup_check(&a)?; + let p2 = read_bls_g2_point_no_subgroup_check(&b)?; + Ok(encode_bls_g2_point(&(p1 + p2))) + } + + #[inline] + fn bls12_381_g2_msm( + &self, + pairs: &[(([u8; 48], [u8; 48], [u8; 48], [u8; 48]), [u8; 32])], + ) -> Result<[u8; 192], CryptoError> { + let mut points = Vec::with_capacity(pairs.len()); + let mut scalars = Vec::with_capacity(pairs.len()); + for (point, scalar) in pairs { + points.push(read_bls_g2_point(point)?); + scalars.push(read_bls_scalar(scalar)); + } + + if points.is_empty() { + return Ok([0u8; BLS_G2_LEN]); + } + + Ok(encode_bls_g2_point(&openvm_ecc_guest::msm( + &scalars, &points, + ))) + } + + #[inline] + fn bls12_381_pairing_check( + &self, + pairs: &[( + ([u8; 48], [u8; 48]), + ([u8; 48], [u8; 48], [u8; 48], [u8; 48]), + )], + ) -> Result { + if pairs.is_empty() { + return Ok(true); + } + + let mut g1_points = Vec::with_capacity(pairs.len()); + let mut g2_points = Vec::with_capacity(pairs.len()); + for (g1_bytes, g2_bytes) in pairs { + let (g1_x, g1_y) = read_bls_g1_point(g1_bytes)?.into_coords(); + let (g2_x, g2_y) = read_bls_g2_point(g2_bytes)?.into_coords(); + g1_points.push(AffinePoint::new(g1_x, g1_y)); + g2_points.push(AffinePoint::new(g2_x, g2_y)); + } + + Ok(Bls12_381::pairing_check(&g1_points, &g2_points).is_ok()) + } + + #[inline] + fn bls12_381_fp_to_g1(&self, fp: &[u8; 48]) -> Result<[u8; 96], CryptoError> { + type Fp = ::Field; + + let fp_elem = Fp::from_bytes(fp) + .into_option() + .ok_or(CryptoError::InvalidInput("invalid Fp element"))?; + + let point = bls12_381::G1Projective::map_to_curve(&fp_elem).clear_h(); + serialize_bls12_g1(&bls12_381::G1Affine::from(point)) + } + + #[inline] + fn bls12_381_fp2_to_g2(&self, fp2: ([u8; 48], [u8; 48])) -> Result<[u8; 192], CryptoError> { + type Fp = ::Field; + type Fp2 = ::Field; + + let c0 = Fp::from_bytes(&fp2.0) + .into_option() + .ok_or(CryptoError::InvalidInput("invalid Fp2.c0 element"))?; + let c1 = Fp::from_bytes(&fp2.1) + .into_option() + .ok_or(CryptoError::InvalidInput("invalid Fp2.c1 element"))?; + + let fp2_elem = Fp2 { c0, c1 }; + let point = bls12_381::G2Projective::map_to_curve(&fp2_elem).clear_h(); + serialize_bls12_g2(&bls12_381::G2Affine::from(point)) + } +} + +/// Returns true if the modulus (big-endian, possibly with leading zeros) equals BN254 Fr. +fn is_bn254_fr(modulus: &[u8]) -> bool { + // Strip leading zeros + let stripped = match modulus.iter().position(|&b| b != 0) { + Some(i) => &modulus[i..], + None => return false, // all zeros + }; + // bn::Scalar::MODULUS is little-endian; compare against reversed input + stripped.len() == BN_SCALAR_LEN + && stripped + .iter() + .rev() + .eq(bn::Scalar::MODULUS.as_ref().iter()) +} + +/// Accelerated modexp for BN254 Fr using field arithmetic intrinsics. +fn accelerated_modexp_bn254_fr(base: &[u8], exp: &[u8]) -> Vec { + use openvm_ecc_guest::algebra::{ExpBytes, Reduce}; + + // OpenVM's field reduction requires inputs to be aligned to the field byte size. + let padded_len = base + .len() + .next_multiple_of(BN_SCALAR_LEN) + .max(BN_SCALAR_LEN); + let mut padded = vec![0u8; padded_len]; + padded[padded_len - base.len()..].copy_from_slice(base); + let base_fr = bn::Scalar::reduce_be_bytes(&padded); + + base_fr.exp_bytes(true, exp).to_be_bytes().as_ref().to_vec() +} + +// Helper functions for BN254 operations + +#[inline] +fn read_bn_fq(input: &[u8]) -> Result { + if input.len() < BN_FQ_LEN { + Err(CryptoError::InvalidInput("BN254 fp must be 32 bytes")) + } else { + bn::Fp::from_be_bytes(&input[..BN_FQ_LEN]) + .ok_or(CryptoError::InvalidInput("element not in BN254 base field")) + } +} + +#[inline] +fn read_bn_fq2(input: &[u8]) -> Result { + let y = read_bn_fq(&input[..BN_FQ_LEN])?; + let x = read_bn_fq(&input[BN_FQ_LEN..BN_FQ_LEN * 2])?; + Ok(bn::Fp2::new(x, y)) +} + +#[inline] +fn read_bn_g1_point(input: &[u8]) -> Result { + if input.len() != BN_G1_LEN { + return Err(CryptoError::InvalidInput("BN254 G1 point must be 64 bytes")); + } + let px = read_bn_fq(&input[0..BN_FQ_LEN])?; + let py = read_bn_fq(&input[BN_FQ_LEN..BN_G1_LEN])?; + // SAFETY: `read_bn_fq` produces canonical Fp elements; `from_xy` itself checks the curve + // equation and returns `None` if `(px, py)` is not on the curve. + let point = unsafe { bn::G1Affine::from_xy(px, py) } + .ok_or(CryptoError::InvalidPoint("BN254 G1 point not on curve"))?; + if point.is_in_correct_subgroup() { + Ok(point) + } else { + Err(CryptoError::InvalidPoint("BN254 G1 point not in subgroup")) + } +} + +#[inline] +fn read_bn_g2_point(input: &[u8]) -> Result { + if input.len() != BN_G2_LEN { + return Err(CryptoError::InvalidInput( + "BN254 G2 point must be 128 bytes", + )); + } + let c0 = read_bn_fq2(&input[0..BN_G1_LEN])?; + let c1 = read_bn_fq2(&input[BN_G1_LEN..BN_G2_LEN])?; + // SAFETY: `read_bn_fq2` produces canonical Fp2 elements; `from_xy` itself checks the curve + // equation and returns `None` if `(c0, c1)` is not on the twist. + let point = unsafe { bn::G2Affine::from_xy(c0, c1) } + .ok_or(CryptoError::InvalidPoint("BN254 G2 point not on curve"))?; + if point.is_in_correct_subgroup() { + Ok(point) + } else { + Err(CryptoError::InvalidPoint("BN254 G2 point not in subgroup")) + } +} + +#[inline] +fn encode_bn_g1_point(point: bn::G1Affine) -> [u8; BN_G1_LEN] { + let mut output = [0u8; BN_G1_LEN]; + + let x_bytes: &[u8] = point.x().as_le_bytes(); + let y_bytes: &[u8] = point.y().as_le_bytes(); + for i in 0..BN_FQ_LEN { + output[i] = x_bytes[BN_FQ_LEN - 1 - i]; + output[i + BN_FQ_LEN] = y_bytes[BN_FQ_LEN - 1 - i]; + } + output +} + +/// Reads a scalar from the input slice. The scalar does not need to be canonical. +#[inline] +fn read_bn_scalar(input: &[u8]) -> Result { + if input.len() != BN_SCALAR_LEN { + return Err(CryptoError::InvalidInput("BN254 scalar must be 32 bytes")); + } + Ok(bn::Scalar::from_be_bytes_unchecked(input)) +} + +// Helper functions for BLS12-381 operations + +#[inline] +fn read_bls_fp(input: &[u8; 48]) -> Result { + bls::Fp::from_be_bytes(input).ok_or(CryptoError::InvalidInput( + "element not in BLS12-381 base field", + )) +} + +#[inline] +fn read_bls_fp2(c0: &[u8; 48], c1: &[u8; 48]) -> Result { + let real = read_bls_fp(c0)?; + let imag = read_bls_fp(c1)?; + Ok(bls::Fp2::new(real, imag)) +} + +#[inline] +fn read_bls_g1_point_no_subgroup_check( + point: &([u8; 48], [u8; 48]), +) -> Result { + let px = read_bls_fp(&point.0)?; + let py = read_bls_fp(&point.1)?; + // SAFETY: `read_bls_fp` produces canonical Fp elements; `from_xy` itself checks the curve + // equation and returns `None` if `(px, py)` is not on the curve. + unsafe { bls::G1Affine::from_xy(px, py) } + .ok_or(CryptoError::InvalidPoint("BLS12-381 G1 point not on curve")) +} + +#[inline] +fn read_bls_g1_point(point: &([u8; 48], [u8; 48])) -> Result { + let point = read_bls_g1_point_no_subgroup_check(point)?; + if point.is_in_correct_subgroup() { + Ok(point) + } else { + Err(CryptoError::InvalidPoint( + "BLS12-381 G1 point not in subgroup", + )) + } +} + +#[inline] +fn read_bls_g2_point_no_subgroup_check( + point: &([u8; 48], [u8; 48], [u8; 48], [u8; 48]), +) -> Result { + let x = read_bls_fp2(&point.0, &point.1)?; + let y = read_bls_fp2(&point.2, &point.3)?; + // SAFETY: `read_bls_fp2` produces canonical Fp2 elements; `from_xy` itself checks the curve + // equation and returns `None` if `(x, y)` is not on the twist. + unsafe { bls::G2Affine::from_xy(x, y) } + .ok_or(CryptoError::InvalidPoint("BLS12-381 G2 point not on curve")) +} + +#[inline] +fn read_bls_g2_point( + point: &([u8; 48], [u8; 48], [u8; 48], [u8; 48]), +) -> Result { + let point = read_bls_g2_point_no_subgroup_check(point)?; + if point.is_in_correct_subgroup() { + Ok(point) + } else { + Err(CryptoError::InvalidPoint( + "BLS12-381 G2 point not in subgroup", + )) + } +} + +/// Reads a scalar from the input bytes. The scalar does not need to be canonical. +#[inline] +fn read_bls_scalar(input: &[u8; 32]) -> bls::Scalar { + bls::Scalar::from_be_bytes_unchecked(input) +} + +#[inline] +fn encode_bls_g1_point(point: &bls::G1Affine) -> [u8; BLS_G1_LEN] { + if point.is_identity() { + return [0u8; BLS_G1_LEN]; + } + + let mut output = [0u8; BLS_G1_LEN]; + let x_bytes: &[u8] = point.x().as_le_bytes(); + let y_bytes: &[u8] = point.y().as_le_bytes(); + for i in 0..BLS_FP_LEN { + output[i] = x_bytes[BLS_FP_LEN - 1 - i]; + output[i + BLS_FP_LEN] = y_bytes[BLS_FP_LEN - 1 - i]; + } + output +} + +#[inline] +fn encode_bls_g2_point(point: &bls::G2Affine) -> [u8; BLS_G2_LEN] { + if point.is_identity() { + return [0u8; BLS_G2_LEN]; + } + + let mut output = [0u8; BLS_G2_LEN]; + let x = point.x(); + let y = point.y(); + let x_c0 = x.c0.as_le_bytes(); + let x_c1 = x.c1.as_le_bytes(); + let y_c0 = y.c0.as_le_bytes(); + let y_c1 = y.c1.as_le_bytes(); + for i in 0..BLS_FP_LEN { + output[i] = x_c0[BLS_FP_LEN - 1 - i]; + output[i + BLS_FP_LEN] = x_c1[BLS_FP_LEN - 1 - i]; + output[i + (2 * BLS_FP_LEN)] = y_c0[BLS_FP_LEN - 1 - i]; + output[i + (3 * BLS_FP_LEN)] = y_c1[BLS_FP_LEN - 1 - i]; + } + output +} + +/// Serialize a BLS12-381 G1Affine point to 96 unpadded bytes (x || y, each 48 bytes). +fn serialize_bls12_g1(point: &bls12_381::G1Affine) -> Result<[u8; 96], CryptoError> { + if bool::from(point.is_identity()) { + return Ok([0u8; 96]); + } + + Ok(point.to_uncompressed()) +} + +/// Serialize a BLS12-381 G2Affine point to 192 unpadded bytes. +/// bls12_381 serializes as x_1 || x_0 || y_1 || y_0 (192 bytes). +/// We output as x_0 || x_1 || y_0 || y_1 to match EIP-2537 convention. +fn serialize_bls12_g2(point: &bls12_381::G2Affine) -> Result<[u8; 192], CryptoError> { + if bool::from(point.is_identity()) { + return Ok([0u8; 192]); + } + + let raw = point.to_uncompressed(); + let mut out = [0u8; 192]; + out[0..48].copy_from_slice(&raw[48..96]); // x_0 + out[48..96].copy_from_slice(&raw[0..48]); // x_1 + out[96..144].copy_from_slice(&raw[144..192]); // y_0 + out[144..192].copy_from_slice(&raw[96..144]); // y_1 + Ok(out) +} diff --git a/crates/guest-program/stateless-validator/src/crypto/zkvm_interface.rs b/crates/guest-program/stateless-validator/src/crypto/zkvm_interface.rs new file mode 100644 index 00000000000..a532da972f5 --- /dev/null +++ b/crates/guest-program/stateless-validator/src/crypto/zkvm_interface.rs @@ -0,0 +1,356 @@ +//! [`ethrex_crypto::Crypto`] implementation using [`zkvm_interface`]. + +use alloc::{string::ToString, sync::Arc, vec, vec::Vec}; +use core::mem::transmute; + +use ethrex_crypto::{Crypto, CryptoError}; +use zkvm_interface::{ + zkvm_blake2f, zkvm_blake2f_message, zkvm_blake2f_offset, zkvm_blake2f_state, zkvm_bls12_381_fp, + zkvm_bls12_381_fp2, zkvm_bls12_381_g1_msm_pair, zkvm_bls12_381_g1_point, + zkvm_bls12_381_g2_msm_pair, zkvm_bls12_381_g2_point, zkvm_bls12_381_pairing_pair, + zkvm_bls12_381_scalar, zkvm_bls12_g1_add, zkvm_bls12_g1_msm, zkvm_bls12_g2_add, + zkvm_bls12_g2_msm, zkvm_bls12_map_fp_to_g1, zkvm_bls12_map_fp2_to_g2, zkvm_bls12_pairing, + zkvm_bn254_g1_add, zkvm_bn254_g1_mul, zkvm_bn254_g1_point, zkvm_bn254_g2_point, + zkvm_bn254_pairing, zkvm_bn254_pairing_pair, zkvm_bn254_scalar, zkvm_keccak256, + zkvm_keccak256_hash, zkvm_kzg_commitment, zkvm_kzg_field_element, zkvm_kzg_point_eval, + zkvm_kzg_proof, zkvm_modexp, zkvm_ripemd160, zkvm_ripemd160_hash, zkvm_secp256k1_ecrecover, + zkvm_secp256k1_hash, zkvm_secp256k1_pubkey, zkvm_secp256k1_signature, zkvm_secp256r1_hash, + zkvm_secp256r1_pubkey, zkvm_secp256r1_signature, zkvm_secp256r1_verify, zkvm_sha256, + zkvm_sha256_hash, +}; + +/// Returns a [`Crypto`] implementation backed by [`zkvm_interface`] syscalls. +#[inline] +pub(super) fn crypto() -> Arc { + Arc::new(ZkVMInterfaceCrypto) +} + +#[derive(Debug, Default)] +struct ZkVMInterfaceCrypto; + +impl Crypto for ZkVMInterfaceCrypto { + #[inline] + fn secp256k1_ecrecover( + &self, + sig: &[u8; 64], + recid: u8, + msg: &[u8; 32], + ) -> Result<[u8; 32], CryptoError> { + let msg = zkvm_secp256k1_hash { data: *msg }; + let sig = zkvm_secp256k1_signature { data: *sig }; + let mut pubkey = zkvm_secp256k1_pubkey { data: [0; 64] }; + let ret = unsafe { zkvm_secp256k1_ecrecover(&msg, &sig, recid, &mut pubkey) }; + if ret != 0 { + return Err(CryptoError::RecoveryFailed); + } + Ok(keccak256(&pubkey.data)) + } + + #[inline] + fn keccak256(&self, input: &[u8]) -> [u8; 32] { + keccak256(input) + } + + #[inline] + fn sha256(&self, input: &[u8]) -> [u8; 32] { + let mut output = zkvm_sha256_hash { data: [0; 32] }; + let ret = unsafe { zkvm_sha256(input.as_ptr(), input.len(), &mut output) }; + assert_eq!(ret, 0, "sha256 failed"); + output.data + } + + #[inline] + fn ripemd160(&self, input: &[u8]) -> [u8; 32] { + let mut output = zkvm_ripemd160_hash { data: [0; 32] }; + let ret = unsafe { zkvm_ripemd160(input.as_ptr(), input.len(), &mut output) }; + assert_eq!(ret, 0, "ripemd160 failed"); + output.data + } + + #[inline] + fn bn254_g1_add(&self, p1: &[u8], p2: &[u8]) -> Result<[u8; 64], CryptoError> { + let p1: &[u8; 64] = p1 + .try_into() + .map_err(|_| CryptoError::InvalidInput("bn254_g1_add: p1 must be 64 bytes"))?; + let p2: &[u8; 64] = p2 + .try_into() + .map_err(|_| CryptoError::InvalidInput("bn254_g1_add: p2 must be 64 bytes"))?; + let p1 = zkvm_bn254_g1_point { data: *p1 }; + let p2 = zkvm_bn254_g1_point { data: *p2 }; + let mut result = zkvm_bn254_g1_point { data: [0; 64] }; + let ret = unsafe { zkvm_bn254_g1_add(&p1, &p2, &mut result) }; + (ret == 0) + .then_some(result.data) + .ok_or_else(|| CryptoError::Other("bn254_g1_add failed".to_string())) + } + + #[inline] + fn bn254_g1_mul(&self, point: &[u8], scalar: &[u8]) -> Result<[u8; 64], CryptoError> { + let point: &[u8; 64] = point + .try_into() + .map_err(|_| CryptoError::InvalidInput("bn254_g1_mul: point must be 64 bytes"))?; + let scalar: &[u8; 32] = scalar + .try_into() + .map_err(|_| CryptoError::InvalidInput("bn254_g1_mul: scalar must be 32 bytes"))?; + let point = zkvm_bn254_g1_point { data: *point }; + let scalar = zkvm_bn254_scalar { data: *scalar }; + let mut result = zkvm_bn254_g1_point { data: [0; 64] }; + let ret = unsafe { zkvm_bn254_g1_mul(&point, &scalar, &mut result) }; + (ret == 0) + .then_some(result.data) + .ok_or_else(|| CryptoError::Other("bn254_g1_mul failed".to_string())) + } + + #[inline] + fn bn254_pairing_check(&self, pairs: &[(&[u8], &[u8])]) -> Result { + let pairs: Vec = pairs + .iter() + .map(|(g1, g2)| { + let g1: [u8; 64] = (*g1) + .try_into() + .map_err(|_| CryptoError::InvalidInput("bn254_pairing: G1 must be 64 bytes"))?; + let g2: [u8; 128] = (*g2).try_into().map_err(|_| { + CryptoError::InvalidInput("bn254_pairing: G2 must be 128 bytes") + })?; + Ok(zkvm_bn254_pairing_pair { + g1: zkvm_bn254_g1_point { data: g1 }, + g2: zkvm_bn254_g2_point { data: g2 }, + }) + }) + .collect::>()?; + let mut verified = false; + let ret = unsafe { zkvm_bn254_pairing(pairs.as_ptr(), pairs.len(), &mut verified) }; + (ret == 0) + .then_some(verified) + .ok_or_else(|| CryptoError::Other("bn254_pairing failed".to_string())) + } + + #[inline] + fn modexp(&self, base: &[u8], exp: &[u8], modulus: &[u8]) -> Result, CryptoError> { + let mut output = vec![0u8; modulus.len()]; + let ret = unsafe { + zkvm_modexp( + base.as_ptr(), + base.len(), + exp.as_ptr(), + exp.len(), + modulus.as_ptr(), + modulus.len(), + output.as_mut_ptr(), + ) + }; + (ret == 0) + .then_some(output) + .ok_or_else(|| CryptoError::Other("modexp failed".to_string())) + } + + #[cfg(feature = "zisk")] + #[inline] + fn mulmod256(&self, a: &[u8; 32], b: &[u8; 32], m: &[u8; 32]) -> [u8; 32] { + // `mul_mod_bytes256_c` is exported by ziskos but not declared in `zkvm_interface`. + unsafe extern "C" { + fn mul_mod_bytes256_c( + a_ptr: *const u8, + b_ptr: *const u8, + m_ptr: *const u8, + result_ptr: *mut u8, + ); + } + + let mut result = [0u8; 32]; + unsafe { mul_mod_bytes256_c(a.as_ptr(), b.as_ptr(), m.as_ptr(), result.as_mut_ptr()) }; + result + } + + #[inline] + fn blake2_compress(&self, rounds: u32, h: &mut [u64; 8], m: [u64; 16], t: [u64; 2], f: bool) { + let mut state = zkvm_blake2f_state { + data: unsafe { transmute::<[u64; 8], [u8; 64]>(*h) }, + }; + let m = zkvm_blake2f_message { + data: unsafe { transmute::<[u64; 16], [u8; 128]>(m) }, + }; + let t = zkvm_blake2f_offset { + data: unsafe { transmute::<[u64; 2], [u8; 16]>(t) }, + }; + let ret = unsafe { zkvm_blake2f(rounds, &mut state, &m, &t, f as u8) }; + assert_eq!(ret, 0, "blake2f failed"); + *h = unsafe { transmute::<[u8; 64], [u64; 8]>(state.data) }; + } + + #[inline] + fn secp256r1_verify(&self, msg: &[u8; 32], sig: &[u8; 64], pk: &[u8; 64]) -> bool { + let msg = zkvm_secp256r1_hash { data: *msg }; + let sig = zkvm_secp256r1_signature { data: *sig }; + let pk = zkvm_secp256r1_pubkey { data: *pk }; + let mut verified = false; + let ret = unsafe { zkvm_secp256r1_verify(&msg, &sig, &pk, &mut verified) }; + ret == 0 && verified + } + + #[inline] + fn verify_kzg_proof( + &self, + z: &[u8; 32], + y: &[u8; 32], + commitment: &[u8; 48], + proof: &[u8; 48], + ) -> Result<(), CryptoError> { + let commitment = zkvm_kzg_commitment { data: *commitment }; + let z = zkvm_kzg_field_element { data: *z }; + let y = zkvm_kzg_field_element { data: *y }; + let proof = zkvm_kzg_proof { data: *proof }; + let mut verified = false; + let ret = unsafe { zkvm_kzg_point_eval(&commitment, &z, &y, &proof, &mut verified) }; + if ret != 0 { + return Err(CryptoError::Other( + "KZG point eval syscall failed".to_string(), + )); + } + if !verified { + return Err(CryptoError::VerificationFailed); + } + Ok(()) + } + + #[inline] + fn bls12_381_g1_add( + &self, + a: ([u8; 48], [u8; 48]), + b: ([u8; 48], [u8; 48]), + ) -> Result<[u8; 96], CryptoError> { + let a = pack_bls12_381_g1(a); + let b = pack_bls12_381_g1(b); + let mut result = zkvm_bls12_381_g1_point { data: [0; 96] }; + let ret = unsafe { zkvm_bls12_g1_add(&a, &b, &mut result) }; + (ret == 0) + .then_some(result.data) + .ok_or_else(|| CryptoError::Other("bls12_g1_add failed".to_string())) + } + + #[inline] + fn bls12_381_g1_msm( + &self, + pairs: &[(([u8; 48], [u8; 48]), [u8; 32])], + ) -> Result<[u8; 96], CryptoError> { + let pairs: Vec = pairs + .iter() + .map(|(point, scalar)| zkvm_bls12_381_g1_msm_pair { + point: pack_bls12_381_g1(*point), + scalar: zkvm_bls12_381_scalar { data: *scalar }, + }) + .collect(); + let mut result = zkvm_bls12_381_g1_point { data: [0; 96] }; + let ret = unsafe { zkvm_bls12_g1_msm(pairs.as_ptr(), pairs.len(), &mut result) }; + (ret == 0) + .then_some(result.data) + .ok_or_else(|| CryptoError::Other("bls12_g1_msm failed".to_string())) + } + + #[inline] + fn bls12_381_g2_add( + &self, + a: ([u8; 48], [u8; 48], [u8; 48], [u8; 48]), + b: ([u8; 48], [u8; 48], [u8; 48], [u8; 48]), + ) -> Result<[u8; 192], CryptoError> { + let a = pack_bls12_381_g2(a); + let b = pack_bls12_381_g2(b); + let mut result = zkvm_bls12_381_g2_point { data: [0; 192] }; + let ret = unsafe { zkvm_bls12_g2_add(&a, &b, &mut result) }; + (ret == 0) + .then_some(result.data) + .ok_or_else(|| CryptoError::Other("bls12_g2_add failed".to_string())) + } + + #[inline] + fn bls12_381_g2_msm( + &self, + pairs: &[(([u8; 48], [u8; 48], [u8; 48], [u8; 48]), [u8; 32])], + ) -> Result<[u8; 192], CryptoError> { + let pairs: Vec = pairs + .iter() + .map(|(point, scalar)| zkvm_bls12_381_g2_msm_pair { + point: pack_bls12_381_g2(*point), + scalar: zkvm_bls12_381_scalar { data: *scalar }, + }) + .collect(); + let mut result = zkvm_bls12_381_g2_point { data: [0; 192] }; + let ret = unsafe { zkvm_bls12_g2_msm(pairs.as_ptr(), pairs.len(), &mut result) }; + (ret == 0) + .then_some(result.data) + .ok_or_else(|| CryptoError::Other("bls12_g2_msm failed".to_string())) + } + + #[inline] + fn bls12_381_pairing_check( + &self, + pairs: &[( + ([u8; 48], [u8; 48]), + ([u8; 48], [u8; 48], [u8; 48], [u8; 48]), + )], + ) -> Result { + let pairs: Vec = pairs + .iter() + .map(|(g1, g2)| zkvm_bls12_381_pairing_pair { + g1: pack_bls12_381_g1(*g1), + g2: pack_bls12_381_g2(*g2), + }) + .collect(); + let mut verified = false; + let ret = unsafe { zkvm_bls12_pairing(pairs.as_ptr(), pairs.len(), &mut verified) }; + (ret == 0) + .then_some(verified) + .ok_or_else(|| CryptoError::Other("bls12_pairing failed".to_string())) + } + + #[inline] + fn bls12_381_fp_to_g1(&self, fp: &[u8; 48]) -> Result<[u8; 96], CryptoError> { + let fp = zkvm_bls12_381_fp { data: *fp }; + let mut result = zkvm_bls12_381_g1_point { data: [0; 96] }; + let ret = unsafe { zkvm_bls12_map_fp_to_g1(&fp, &mut result) }; + (ret == 0) + .then_some(result.data) + .ok_or_else(|| CryptoError::Other("bls12_map_fp_to_g1 failed".to_string())) + } + + #[inline] + fn bls12_381_fp2_to_g2(&self, fp2: ([u8; 48], [u8; 48])) -> Result<[u8; 192], CryptoError> { + let fp2 = { + let mut data = [0u8; 96]; + data[..48].copy_from_slice(&fp2.0); + data[48..].copy_from_slice(&fp2.1); + zkvm_bls12_381_fp2 { data } + }; + let mut result = zkvm_bls12_381_g2_point { data: [0; 192] }; + let ret = unsafe { zkvm_bls12_map_fp2_to_g2(&fp2, &mut result) }; + (ret == 0) + .then_some(result.data) + .ok_or_else(|| CryptoError::Other("bls12_map_fp2_to_g2 failed".to_string())) + } +} + +#[inline] +fn keccak256(data: &[u8]) -> [u8; 32] { + let mut output = zkvm_keccak256_hash { data: [0; 32] }; + let ret = unsafe { zkvm_keccak256(data.as_ptr(), data.len(), &mut output) }; + assert_eq!(ret, 0, "keccak256 failed"); + output.data +} + +#[inline] +fn pack_bls12_381_g1(p: ([u8; 48], [u8; 48])) -> zkvm_bls12_381_g1_point { + let mut data = [0u8; 96]; + data[..48].copy_from_slice(&p.0); + data[48..].copy_from_slice(&p.1); + zkvm_bls12_381_g1_point { data } +} + +#[inline] +fn pack_bls12_381_g2(p: ([u8; 48], [u8; 48], [u8; 48], [u8; 48])) -> zkvm_bls12_381_g2_point { + let mut data = [0u8; 192]; + data[..48].copy_from_slice(&p.0); + data[48..96].copy_from_slice(&p.1); + data[96..144].copy_from_slice(&p.2); + data[144..].copy_from_slice(&p.3); + zkvm_bls12_381_g2_point { data } +} diff --git a/crates/guest-program/stateless-validator/src/lib.rs b/crates/guest-program/stateless-validator/src/lib.rs new file mode 100644 index 00000000000..feadf97272a --- /dev/null +++ b/crates/guest-program/stateless-validator/src/lib.rs @@ -0,0 +1,49 @@ +//! Ethrex stateless-validator guest. +//! +//! Implements the wire contract of the zkEVM stateless-validation spec: decode +//! `statelessInputBytes` (a schema-prefixed SSZ `SszStatelessInput`), run ethrex +//! stateless validation, and emit `statelessOutputBytes` (an SSZ +//! `SszStatelessValidationResult`). +//! +//! Ported from the `feat/stateless-validator-mirror` spike, with one deliberate +//! difference: the spike took eth-act's `stateless-validator-common` as a git +//! dependency for the wire types, whereas ethrex owns them natively in +//! `ethrex_common::types::stateless_ssz`. That also removes the spike's +//! `convert.rs` entirely — its 369 lines existed only to map between two mirror +//! type hierarchies, and with one hierarchy every one of those conversions is the +//! identity. +//! +//! What this crate adds on top of `ethrex_guest_program::l1` is the per-zkVM +//! machinery: a `Crypto` provider per target (see [`crypto`]) and, behind the +//! `ere` feature, the ere-platform entrypoint (see [`platform`]). +//! +//! Target: execution-specs `3c3b6f4af315b268a61e20d5a4da8aa4f24c91f0` +//! (#3248 progressive SSZ + #3278 `ChainConfig` removal). + +#[cfg(any(feature = "ere", feature = "zkvm-interface", feature = "openvm"))] +pub mod crypto; +#[cfg(feature = "ere")] +pub mod platform; + +use std::sync::Arc; + +use ethrex_crypto::Crypto; + +pub use ethrex_common::types::stateless_ssz::{ + STATELESS_INPUT_SCHEMA_ID, SszStatelessInput, SszStatelessValidationResult, +}; +pub use ethrex_guest_program::l1::{StatelessInputDecodeError, decode_stateless_input}; + +/// Run stateless validation over serialized input and return serialized output. +/// +/// A thin wrapper over [`ethrex_guest_program::l1::run_stateless_guest`], which is +/// the single implementation shared by this guest, the `ExecBackend`, and the +/// ef_tests conformance comparison. Keeping one implementation is the point: a +/// second copy is what let the public-key check go missing on the EXECUTE path. +/// +/// Never panics and never returns an error. A decode failure commits the all-zero +/// default result; a decodable input commits the real payload-request root, +/// `chain_id` and `schema_id` even when validation fails. +pub fn run_stateless_validation(input_bytes: &[u8], crypto: Arc) -> Vec { + ethrex_guest_program::l1::run_stateless_guest(input_bytes, crypto) +} diff --git a/crates/guest-program/stateless-validator/src/platform.rs b/crates/guest-program/stateless-validator/src/platform.rs new file mode 100644 index 00000000000..d24ccd8f5d1 --- /dev/null +++ b/crates/guest-program/stateless-validator/src/platform.rs @@ -0,0 +1,95 @@ +//! ere-platform entrypoint for the stateless-validator guest. +//! +//! Per-zkVM bins call [`entrypoint`] with their `Platform` implementation, which +//! supplies the input/output plumbing and cycle-count instrumentation; the crypto +//! provider is selected by cargo feature instead (see [`crate::crypto`]). +//! +//! Taking `ere-platform-core` rather than hand-mirroring each zkVM's read/write +//! convention makes the IO contract with `ere-server` structural. The scope names +//! below appear in ZisK profiling output, so keep them stable. + +use ethrex_crypto::Crypto; +use libssz::SszEncode as _; +use std::sync::Arc; + +pub use ere_platform_core::Platform; + +use crate::{SszStatelessInput, SszStatelessValidationResult}; + +/// Runs the stateless guest on the [`Platform`]. +pub fn entrypoint() { + let input_bytes = P::cycle_scope("read_input", || P::read_input()); + let output_bytes = run_stateless_guest::

(&input_bytes); + P::cycle_scope("write_output", || P::write_output(&output_bytes)); +} + +/// Runs the stateless guest with serialized input and returns serialized output, +/// mirroring `run_stateless_guest` in the spec, with per-stage cycle scopes. +/// +/// Structurally identical to [`crate::run_stateless_validation`] — the only +/// difference is the instrumentation. `tests/platform_parity.rs` asserts the two +/// produce byte-identical output, so the scopes cannot silently change behaviour. +pub fn run_stateless_guest(input_bytes: &[u8]) -> Vec { + let crypto = crate::crypto::crypto(); + + let Ok(input) = P::cycle_scope("deserialize_input", || { + crate::decode_stateless_input(input_bytes) + }) else { + let mut out = Vec::new(); + SszStatelessValidationResult::default().ssz_append(&mut out); + return out; + }; + + let new_payload_request_root = P::cycle_scope("new_payload_request_root", || { + new_payload_request_root(&input, crypto.clone()) + }); + let chain_id = input.chain_id; + + // No `validate_chain_config` scope: #3278 removed all chain configuration + // from the wire, so there is nothing host-supplied left to validate. The fork + // is fixed by the schema id and the config is derived inside the validation. + let successful_validation = verify_stateless_new_payload::

(&input, crypto).is_ok(); + + let output = SszStatelessValidationResult { + new_payload_request_root, + successful_validation, + chain_id, + schema_id: crate::STATELESS_INPUT_SCHEMA_ID, + }; + + P::cycle_scope("serialize_output", || { + let mut out = Vec::new(); + output.ssz_append(&mut out); + out + }) +} + +/// Computes the payload-request root committed in the output. +fn new_payload_request_root(input: &SszStatelessInput, crypto: Arc) -> [u8; 32] { + use libssz_merkle::{HashTreeRoot, Sha256Hasher}; + + struct CryptoHasher(Arc); + impl Sha256Hasher for CryptoHasher { + fn hash(&self, data: &[u8]) -> [u8; 32] { + self.0.sha256(data) + } + } + + input + .new_payload_request + .hash_tree_root(&CryptoHasher(crypto)) +} + +/// Statelessly validates the execution payload, with a cycle scope around the +/// expensive part. +fn verify_stateless_new_payload( + input: &SszStatelessInput, + crypto: Arc, +) -> Result<(), ethrex_guest_program::common::ExecutionError> { + P::cycle_scope("run_validation", || { + ethrex_guest_program::l1::validate_stateless_execution(input, crypto).map_err(|err| { + P::print(&format!("Validation failed: {err}\n")); + err + }) + }) +} diff --git a/crates/guest-program/stateless-validator/tests/common/mod.rs b/crates/guest-program/stateless-validator/tests/common/mod.rs new file mode 100644 index 00000000000..b8a098e3c80 --- /dev/null +++ b/crates/guest-program/stateless-validator/tests/common/mod.rs @@ -0,0 +1,105 @@ +//! Shared fixture loading for the host-side tests. +//! +//! Fixtures are EEST `blockchain_test` JSON files embedding +//! `statelessInputBytes`/`statelessOutputBytes` (the `tests-zkevm` releases +//! of `ethereum/execution-specs`); they are downloaded out of band and pointed +//! to by the `ETHREX_STATELESS_FIXTURES` environment variable, never +//! committed. + +use std::{ + collections::BTreeMap, + fs, + path::{Path, PathBuf}, +}; + +use serde::{Deserialize, Deserializer}; + +pub const FIXTURES_DIR_ENV: &str = "ETHREX_STATELESS_FIXTURES"; + +/// A fixture normalized to canonical schema-prefixed SSZ input and expected +/// output bytes, mirroring the loader in ere-guests' stateless-validator-test. +pub struct Fixture { + pub name: String, + pub stateless_input_bytes: Vec, + pub stateless_output_bytes: Vec, +} + +type EestFixtureFile = BTreeMap; + +/// Minimal projection of an EEST `blockchain_test` body. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct EestTest { + blocks: Vec, +} + +/// Minimal projection of a single EEST block. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct EestBlock { + #[serde(default, deserialize_with = "opt_hex_bytes")] + stateless_input_bytes: Option>, + #[serde(default, deserialize_with = "opt_hex_bytes")] + stateless_output_bytes: Option>, +} + +/// Deserializes an optional `0x`-prefixed hex string into bytes. +fn opt_hex_bytes<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + let Some(s) = Option::::deserialize(deserializer)? else { + return Ok(None); + }; + hex::decode(s.trim_start_matches("0x")) + .map(Some) + .map_err(serde::de::Error::custom) +} + +/// Recursively collects every `.json` fixture file under `dir`. +fn fixture_files(dir: &Path, files: &mut Vec) { + for entry in fs::read_dir(dir).expect("fixture directory should be readable") { + let path = entry + .expect("fixture directory entry should be readable") + .path(); + if path.is_dir() { + fixture_files(&path, files); + } else if path.extension().is_some_and(|ext| ext == "json") { + files.push(path); + } + } +} + +/// Loads every fixture under `dir`, sorted by name for determinism. Blocks +/// without embedded stateless bytes (e.g. pre-zkevm fixtures) are skipped. +pub fn load_fixtures(dir: &Path) -> Vec { + let mut files = Vec::new(); + fixture_files(dir, &mut files); + let mut fixtures: Vec = files + .iter() + .flat_map(|path| { + let bytes = fs::read(path).expect("fixture file should be readable"); + let tests: EestFixtureFile = + serde_json::from_slice(&bytes).expect("fixture file should be valid EEST JSON"); + tests + .into_iter() + .flat_map(|(test_id, test)| { + test.blocks + .into_iter() + .enumerate() + .filter_map(move |(idx, block)| { + let input = block.stateless_input_bytes?; + let output = block.stateless_output_bytes?; + (!input.is_empty()).then(|| Fixture { + name: format!("{test_id}#block{idx}"), + stateless_input_bytes: input, + stateless_output_bytes: output, + }) + }) + }) + .collect::>() + }) + .collect(); + fixtures.sort_by(|a, b| a.name.cmp(&b.name)); + fixtures +} diff --git a/crates/guest-program/stateless-validator/tests/host_fixtures.rs b/crates/guest-program/stateless-validator/tests/host_fixtures.rs new file mode 100644 index 00000000000..ce06100d15b --- /dev/null +++ b/crates/guest-program/stateless-validator/tests/host_fixtures.rs @@ -0,0 +1,71 @@ +//! Host-side fixture tests for the stateless-validator guest logic. +//! +//! Runs `run_stateless_validation` over EEST `blockchain_test` fixtures that +//! embed `statelessInputBytes`/`statelessOutputBytes` (the `tests-zkevm` +//! releases of `ethereum/execution-specs`) and asserts the produced output is +//! byte-identical to the expected output. This is both the PR gate for guest +//! breakage and the equivalence harness for comparing guest integrations. +//! +//! See `tests/common/mod.rs` for the fixture source and the +//! `ETHREX_STATELESS_FIXTURES` contract; when the variable is unset the test +//! is skipped so plain `cargo test` runs stay green without a download. Point it +//! at the `blockchain_tests/` subtree of a `make -C tooling/ef_tests/blockchain +//! stateless-vector` run — not its parent, which also holds a `.meta/index.json` +//! that is not a fixture. +//! +//! MEASURED BASELINE, 2026-08-05, against the 769-block generated vector set: +//! 8 exact matches, 755 differing **only** in bytes 0..32 +//! (`new_payload_request_root`), 6 differing more widely. +//! +//! The 755 are all explained by one upstream defect: `libssz-merkle 0.2.2` +//! reverses the progressive-merkleization subtree children, so every +//! `hash_tree_root` over a `ProgressiveContainer` is wrong. On those blocks +//! `successful_validation`, `chain_id` and `schema_id` are already byte-identical +//! to the reference — including on true-success cases — so decode, witness +//! rebuild, public-key validation, block reconstruction and execution all agree +//! with execution-specs today. See `test/tests/common/progressive_ssz_tests.rs` +//! for the proof and the one-line fix. Expect ~763/769 once it lands; the +//! remaining 6 need separate investigation. +#![cfg(feature = "host")] + +mod common; + +use std::{path::Path, sync::Arc}; + +use ethrex_guest_program::crypto::NativeCrypto; +use ethrex_stateless_validator::run_stateless_validation; + +#[test] +fn eest_fixture_equivalence() { + let Some(dir) = std::env::var_os(common::FIXTURES_DIR_ENV) else { + eprintln!( + "skipping eest_fixture_equivalence: set {} to a directory of \ + tests-zkevm blockchain_test fixtures", + common::FIXTURES_DIR_ENV + ); + return; + }; + let fixtures = common::load_fixtures(Path::new(&dir)); + assert!( + !fixtures.is_empty(), + "no fixtures with stateless bytes found under {}", + common::FIXTURES_DIR_ENV + ); + + let crypto = Arc::new(NativeCrypto); + let mut failures = Vec::new(); + for fixture in &fixtures { + let output = run_stateless_validation(&fixture.stateless_input_bytes, crypto.clone()); + if output != fixture.stateless_output_bytes { + failures.push(fixture.name.clone()); + } + } + assert!( + failures.is_empty(), + "{}/{} fixtures produced output bytes different from the expected \ + statelessOutputBytes: {failures:#?}", + failures.len(), + fixtures.len(), + ); + println!("{} fixtures matched expected output bytes", fixtures.len()); +} diff --git a/crates/guest-program/stateless-validator/tests/platform_parity.rs b/crates/guest-program/stateless-validator/tests/platform_parity.rs new file mode 100644 index 00000000000..19822831e0d --- /dev/null +++ b/crates/guest-program/stateless-validator/tests/platform_parity.rs @@ -0,0 +1,51 @@ +//! Byte-parity between the ere-platform entrypoint path (mirror spike) and +//! the plain runner over the same fixtures. `TestPlatform` uses the trait's +//! no-op defaults for print/cycle scopes, so only the shared validation path +//! is exercised — the zkVM IO defaults are never invoked. +#![cfg(all(feature = "host", feature = "ere"))] + +mod common; + +use std::{path::Path, sync::Arc}; + +use ethrex_guest_program::crypto::NativeCrypto; +use ethrex_stateless_validator::platform::{Platform, run_stateless_guest}; +use ethrex_stateless_validator::run_stateless_validation; + +struct TestPlatform; + +impl Platform for TestPlatform {} + +#[test] +fn platform_path_matches_plain_runner() { + let Some(dir) = std::env::var_os(common::FIXTURES_DIR_ENV) else { + eprintln!( + "skipping platform_path_matches_plain_runner: set {} to a directory of \ + tests-zkevm blockchain_test fixtures", + common::FIXTURES_DIR_ENV + ); + return; + }; + let fixtures = common::load_fixtures(Path::new(&dir)); + assert!( + !fixtures.is_empty(), + "no fixtures with stateless bytes found" + ); + + let crypto = Arc::new(NativeCrypto); + for fixture in &fixtures { + let platform_output = run_stateless_guest::(&fixture.stateless_input_bytes); + let plain_output = run_stateless_validation(&fixture.stateless_input_bytes, crypto.clone()); + assert_eq!( + platform_output, plain_output, + "platform and plain runner diverged on {}", + fixture.name + ); + assert_eq!( + platform_output, fixture.stateless_output_bytes, + "platform runner output differs from expected statelessOutputBytes on {}", + fixture.name + ); + } + println!("{} fixtures matched across both paths", fixtures.len()); +} From 9624d70d23d809978a9c4f54029cde7e4003841c Mon Sep 17 00:00:00 2001 From: Lucas Fiegl Date: Wed, 5 Aug 2026 10:21:36 -0300 Subject: [PATCH 15/30] ci(l1): publish stateless-validator ELFs and VKs --- .github/scripts/extract-stateless-fixture.sh | 62 +++++ .github/scripts/zkvm-version.sh | 45 ++++ .github/workflows/tag_release.yaml | 213 ++++++++++-------- .gitignore | 3 + .../stateless-validator/bin/openvm/Cargo.toml | 48 ++++ .../bin/openvm/openvm_init.rs | 4 + .../bin/openvm/src/main.rs | 10 + .../stateless-validator/bin/sp1/Cargo.toml | 27 +++ .../stateless-validator/bin/sp1/src/main.rs | 12 + .../stateless-validator/bin/zisk/Cargo.toml | 22 ++ .../stateless-validator/bin/zisk/src/main.rs | 12 + 11 files changed, 361 insertions(+), 97 deletions(-) create mode 100755 .github/scripts/extract-stateless-fixture.sh create mode 100755 .github/scripts/zkvm-version.sh create mode 100644 crates/guest-program/stateless-validator/bin/openvm/Cargo.toml create mode 100644 crates/guest-program/stateless-validator/bin/openvm/openvm_init.rs create mode 100644 crates/guest-program/stateless-validator/bin/openvm/src/main.rs create mode 100644 crates/guest-program/stateless-validator/bin/sp1/Cargo.toml create mode 100644 crates/guest-program/stateless-validator/bin/sp1/src/main.rs create mode 100644 crates/guest-program/stateless-validator/bin/zisk/Cargo.toml create mode 100644 crates/guest-program/stateless-validator/bin/zisk/src/main.rs diff --git a/.github/scripts/extract-stateless-fixture.sh b/.github/scripts/extract-stateless-fixture.sh new file mode 100755 index 00000000000..584ff4d4394 --- /dev/null +++ b/.github/scripts/extract-stateless-fixture.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# +# Extracts one conformance vector's statelessInputBytes / statelessOutputBytes +# pair into raw binary, for the ere-server acceptance check in tag_release.yaml. +# +# Picks a TRUE-SUCCESS case (successful_validation == 1) deterministically. That +# is not a detail: the root, chain_id and schema_id are all computed before or +# without executing the block, so a guest whose execution is completely broken +# still reproduces a failure vector exactly. Only a success case proves the ELF +# can actually validate a block. +# +# Writes: output/stateless-input.bin, output/expected-output.bin + +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +VECTORS="$ROOT/tooling/ef_tests/blockchain/vectors_stateless_3278/blockchain_tests" +OUT="$ROOT/output" +mkdir -p "$OUT" + +if [[ ! -d $VECTORS ]]; then + echo "No vectors at $VECTORS; run 'make -C tooling/ef_tests/blockchain stateless-vector' first" >&2 + exit 1 +fi + +# `find | sort` for determinism, so a failure is reproducible rather than +# dependent on filesystem order. os-walk rather than a glob because the fill +# output nests fixtures under a dot-prefixed work directory. +FOUND="" +while IFS= read -r file; do + PAIR=$(jq -r ' + to_entries[] | .value as $t + | ($t.blocks // [])[] as $b + | select(($b.statelessInputBytes // "") != "") + | select(($b.statelessOutputBytes // "") != "") + # byte 32 of the SSZ result is successful_validation; hex chars 64..66. + | select(($b.statelessOutputBytes | ltrimstr("0x") | .[64:66]) == "01") + | "\($b.statelessInputBytes)\t\($b.statelessOutputBytes)" + ' "$file" 2>/dev/null | head -1 || true) + if [[ -n $PAIR ]]; then + FOUND="$file" + printf '%s' "${PAIR%%$'\t'*}" | sed 's/^0x//' | xxd -r -p > "$OUT/stateless-input.bin" + printf '%s' "${PAIR##*$'\t'}" | sed 's/^0x//' | xxd -r -p > "$OUT/expected-output.bin" + break + fi +done < <(find "$VECTORS" -name '*.json' | sort) + +if [[ -z $FOUND ]]; then + echo "No true-success vector found under $VECTORS" >&2 + echo "A vector set with no successful_validation==1 case cannot prove the ELF executes." >&2 + exit 1 +fi + +# Belt and braces: assert what we wrote is a 43-byte success result. +LEN=$(wc -c < "$OUT/expected-output.bin" | tr -d ' ') +[[ $LEN -eq 43 ]] || { echo "expected output is $LEN bytes, want 43" >&2; exit 1; } +SUCCESS=$(xxd -p -s 32 -l 1 "$OUT/expected-output.bin") +[[ $SUCCESS == "01" ]] || { echo "expected output is not a success case ($SUCCESS)" >&2; exit 1; } + +echo "Using fixture: ${FOUND#"$ROOT"/}" +echo " input: $(wc -c < "$OUT/stateless-input.bin" | tr -d ' ') bytes" +echo " output: $LEN bytes, successful_validation=1" diff --git a/.github/scripts/zkvm-version.sh b/.github/scripts/zkvm-version.sh new file mode 100755 index 00000000000..9cf4efd2756 --- /dev/null +++ b/.github/scripts/zkvm-version.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# +# Prints the zkVM SDK version the stateless-validator guest is built against, for +# use in release asset names. +# +# The guests reach their SDK through `ere-platform-{zisk,sp1,openvm}`, so there is +# no direct `tag = "vX.Y.Z"` in their manifests to read. The versions are instead +# fixed by the pinned `ere` revision below, mirroring what `ere-catalog` resolves +# at that rev. Bumping the rev therefore REQUIRES updating this table, and the +# consistency check enforces that: it fails if the manifests disagree with +# ERE_REV, so a silent rev bump cannot mislabel an artifact. +# +# Usage: zkvm-version.sh + +set -euo pipefail + +ERE_REV=a25f1aed9664c3b63e73ef05360090a4c41da31b + +# SDK versions resolved by ere-catalog at ERE_REV. +zkvm_version() { + case "$1" in + zisk) echo "1.0.0-alpha" ;; + sp1) echo "6.3.1" ;; + openvm) echo "2.0.0" ;; + *) echo "unknown zkvm: $1" >&2; return 1 ;; + esac +} + +ZKVM="${1:?usage: zkvm-version.sh }" +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +MANIFEST="$ROOT/crates/guest-program/stateless-validator/bin/$ZKVM/Cargo.toml" + +[[ -f $MANIFEST ]] || { echo "no manifest at $MANIFEST" >&2; exit 1; } + +# The table above is only valid for ERE_REV; refuse to guess if it has moved. +if ! grep -q "rev = \"$ERE_REV\"" "$MANIFEST"; then + { + echo "ere rev in $MANIFEST does not match ERE_REV ($ERE_REV)." + echo "Update the zkvm_version table in this script to the SDK versions" + echo "that ere-catalog resolves at the new rev, then update ERE_REV." + } >&2 + exit 1 +fi + +zkvm_version "$ZKVM" diff --git a/.github/workflows/tag_release.yaml b/.github/workflows/tag_release.yaml index 614e726f652..141eb7a1dbe 100644 --- a/.github/workflows/tag_release.yaml +++ b/.github/workflows/tag_release.yaml @@ -149,17 +149,21 @@ jobs: name: verification_keys path: verification_keys/ - # There's a separate job to build the guest programs for SP1, RISC0, and ZisK - # since they need to be built without the l2 features. - build-ethrex-guest: + # Builds the stateless-validator guest ELFs and verification keys through the + # Ere toolchain, so the published artifacts are what eth-act/ere-guests can + # consume directly. Replaces the old build-ethrex-guest/package-ethrex-guest + # pair, which built the L1 batch guest that no longer exists. See issue #7011. + build-stateless-validator-guest: strategy: + fail-fast: false matrix: - # risc0 temporarily disabled: c-kzg 2.1.8 floor exceeds the highest risc0 c-kzg fork tag - # (v2.1.7-risczero.0), so the guest can't build. Re-add "- risc0" once a >=2.1.8 tag exists. zkvm: - - sp1 - zisk + - sp1 + - openvm runs-on: ubuntu-latest + env: + ERE_TAG: 0.14.0 steps: - name: Checkout code uses: actions/checkout@v6 @@ -167,111 +171,123 @@ jobs: - name: Free Disk Space uses: ./.github/actions/free-disk - - name: Setup Rust Environment - uses: ./.github/actions/setup-rust - with: - cache-pool: l2 - - - name: Install SP1 - if: ${{ matrix.zkvm == 'sp1' }} - env: - SHELL: /bin/bash - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - uses: ./.github/actions/install-sp1 - - - name: Install RISC0 - if: ${{ matrix.zkvm == 'risc0' }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - uses: ./.github/actions/install-risc0 - - - name: Install ZisK - if: ${{ matrix.zkvm == 'zisk' }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SETUP_KEY: none - uses: ./.github/actions/install-zisk + - name: Resolve artifact name + id: artifact + run: | + VERSION=$(.github/scripts/zkvm-version.sh ${{ matrix.zkvm }}) + NAME=stateless-validator-ethrex-${{ matrix.zkvm }}-${VERSION} + echo "name=${NAME}" >> $GITHUB_OUTPUT + echo "Artifact name: ${NAME}" - - name: Build ethrex elf - ${{ matrix.zkvm }} - env: - REF_NAME: ${{ github.ref_name }} + - name: Pull Ere images run: | - # Sanitize ref name by replacing '/' with '-' for valid file names - SANITIZED_REF=$(echo "$REF_NAME" | tr '/' '-') - cargo build --release --package ethrex-guest-program --features "${{ matrix.zkvm }}-build-elf,ci" - mkdir -p ${{ matrix.zkvm }}_verification_keys - if [ "${{ matrix.zkvm }}" = "sp1" ]; then - mv crates/guest-program/bin/${{ matrix.zkvm }}/out/riscv32im-succinct-zkvm-elf ethrex-riscv32im-${{ matrix.zkvm }}-elf-$SANITIZED_REF - mv crates/guest-program/bin/${{ matrix.zkvm }}/out/riscv32im-succinct-zkvm-vk-bn254 ${{ matrix.zkvm }}_verification_keys/ethrex-riscv32im-${{ matrix.zkvm }}-vk-bn254-$SANITIZED_REF - mv crates/guest-program/bin/${{ matrix.zkvm }}/out/riscv32im-succinct-zkvm-vk-u32 ${{ matrix.zkvm }}_verification_keys/ethrex-riscv32im-${{ matrix.zkvm }}-vk-u32-$SANITIZED_REF - echo "ELF_ARTIFACT=ethrex-riscv32im-${{ matrix.zkvm }}-elf-$SANITIZED_REF" >> $GITHUB_ENV - elif [ "${{ matrix.zkvm }}" = "risc0" ]; then - mv crates/guest-program/bin/${{ matrix.zkvm }}/out/riscv32im-risc0-elf ethrex-riscv32im-${{ matrix.zkvm }}-elf-$SANITIZED_REF - mv crates/guest-program/bin/${{ matrix.zkvm }}/out/riscv32im-risc0-vk ${{ matrix.zkvm }}_verification_keys/ethrex-riscv32im-${{ matrix.zkvm }}-vk-$SANITIZED_REF - echo "ELF_ARTIFACT=ethrex-riscv32im-${{ matrix.zkvm }}-elf-$SANITIZED_REF" >> $GITHUB_ENV - elif [ "${{ matrix.zkvm }}" = "zisk" ]; then - mv crates/guest-program/bin/${{ matrix.zkvm }}/out/riscv64ima-zisk-elf ethrex-riscv64ima-${{ matrix.zkvm }}-elf-$SANITIZED_REF - echo "ELF_ARTIFACT=ethrex-riscv64ima-${{ matrix.zkvm }}-elf-$SANITIZED_REF" >> $GITHUB_ENV - fi + docker pull ghcr.io/eth-act/ere/ere-compiler-${{ matrix.zkvm }}:${ERE_TAG} + docker pull ghcr.io/eth-act/ere/ere-server-${{ matrix.zkvm }}:${ERE_TAG} - - name: Upload ethrex guest elf artifact - ${{ matrix.zkvm }} - uses: actions/upload-artifact@v6 - with: - name: ${{ env.ELF_ARTIFACT }} - path: ${{ env.ELF_ARTIFACT }} + - name: Compile guest + run: | + mkdir -p output + docker run \ + ${{ matrix.zkvm == 'zisk' && '-e ERE_PROFILE=ethrex' || '' }} \ + -e OPENVM_RUST_TOOLCHAIN=nightly-2026-01-18 \ + -e RUST_LOG=info \ + -v $PWD:/ethrex \ + -v $PWD/output:/output \ + ghcr.io/eth-act/ere/ere-compiler-${{ matrix.zkvm }}:${ERE_TAG} \ + --compiler-kind rust-customized \ + --guest-dir /ethrex/crates/guest-program/stateless-validator/bin/${{ matrix.zkvm }} \ + --output-dir /output \ + --elf-name ${{ steps.artifact.outputs.name }}.elf + + - name: Generate program VK + run: | + docker run \ + -e RUST_LOG=info \ + -v $PWD/output:/output \ + ghcr.io/eth-act/ere/ere-server-${{ matrix.zkvm }}:${ERE_TAG} \ + --elf-path /output/${{ steps.artifact.outputs.name }}.elf \ + keygen \ + --program-vk-path ${{ steps.artifact.outputs.name }}.vk - - name: Upload ethrex guest verification keys - ${{ matrix.zkvm }} + - name: Upload artifact uses: actions/upload-artifact@v6 with: - name: ${{ matrix.zkvm }}_verification_keys - path: ${{ matrix.zkvm }}_verification_keys/ - - # Creates ethrex-guests.tar.gz artifact including SP1, RISC0, and ZisK - # elf and verification keys. - package-ethrex-guest: - needs: - - build-ethrex-guest + name: ${{ steps.artifact.outputs.name }} + path: | + output/${{ steps.artifact.outputs.name }}.elf + output/${{ steps.artifact.outputs.name }}.vk + if-no-files-found: error + + # Proves eth-act/ere-guests can consume the published ELF: runs it under + # ere-server against a generated conformance vector and requires the returned + # statelessOutputBytes to match byte-for-byte. This is the only check that + # exercises the real zkVM IO path, so it is what catches a framing mismatch. + verify-stateless-validator-guest: + needs: build-stateless-validator-guest + strategy: + fail-fast: false + matrix: + zkvm: + - zisk + - sp1 + - openvm runs-on: ubuntu-latest env: - # Sanitize ref name by replacing '/' with '-' to match artifact names - SANITIZED_REF: ${{ github.ref_name }} + ERE_TAG: 0.14.0 steps: - - name: Sanitize ref name - run: | - SANITIZED=$(echo "${{ github.ref_name }}" | tr '/' '-') - echo "SANITIZED_REF=$SANITIZED" >> $GITHUB_ENV + - name: Checkout code + uses: actions/checkout@v6 - - name: Download SP1 elf artifact - uses: actions/download-artifact@v6 - with: - name: ethrex-riscv32im-sp1-elf-${{ env.SANITIZED_REF }} - path: ethrex_guests/sp1/ + - name: Free Disk Space + uses: ./.github/actions/free-disk - - name: Download SP1 verification keys artifacts - uses: actions/download-artifact@v6 - with: - name: sp1_verification_keys - path: ethrex_guests/sp1/ + - name: Setup Rust Environment + uses: ./.github/actions/setup-rust - # risc0 elf/vk downloads temporarily removed: risc0 is disabled (c-kzg 2.1.8 floor exceeds - # the highest risc0 c-kzg fork tag), so build-ethrex-guest no longer produces these artifacts. + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh - - name: Download ZisK elf artifact + - name: Resolve artifact name + id: artifact + run: | + VERSION=$(.github/scripts/zkvm-version.sh ${{ matrix.zkvm }}) + echo "name=stateless-validator-ethrex-${{ matrix.zkvm }}-${VERSION}" >> $GITHUB_OUTPUT + + - name: Download ELF uses: actions/download-artifact@v6 with: - name: ethrex-riscv64ima-zisk-elf-${{ env.SANITIZED_REF }} - path: ethrex_guests/zisk/ + name: ${{ steps.artifact.outputs.name }} + path: output/ - - name: Archive ethrex guests - run: | - cd ethrex_guests/ - tar -czvf ../ethrex-guests.tar.gz . + # execution-specs #3248+#3278 has no tests-zkevm release, so the oracle is + # generated from the pinned spec commit rather than downloaded. + - name: Generate conformance vectors + run: make -C tooling/ef_tests/blockchain stateless-vector - - name: Upload ethrex guests artifact - uses: actions/upload-artifact@v6 - with: - name: ethrex-guests.tar.gz - path: ethrex-guests.tar.gz + - name: Extract a true-success fixture + run: .github/scripts/extract-stateless-fixture.sh + + - name: Execute ELF under ere-server + run: | + docker pull ghcr.io/eth-act/ere/ere-server-${{ matrix.zkvm }}:${ERE_TAG} + docker run \ + -e RUST_LOG=info \ + -v $PWD/output:/output \ + ghcr.io/eth-act/ere/ere-server-${{ matrix.zkvm }}:${ERE_TAG} \ + --elf-path /output/${{ steps.artifact.outputs.name }}.elf \ + execute \ + --input-path /output/stateless-input.bin \ + --output-path /output/stateless-output.bin + + - name: Compare against the fixture + run: | + if ! cmp -s output/stateless-output.bin output/expected-output.bin; then + echo "statelessOutputBytes mismatch" + echo "expected: $(xxd -p output/expected-output.bin | tr -d '\n')" + echo "actual: $(xxd -p output/stateless-output.bin | tr -d '\n')" + exit 1 + fi + echo "statelessOutputBytes match" package-contracts: needs: @@ -407,11 +423,11 @@ jobs: if: github.ref_type == 'tag' && github.event_name != 'workflow_dispatch' needs: - build-ethrex - - build-ethrex-guest + - build-stateless-validator-guest + - verify-stateless-validator-guest - build-docker - publish-docker - package-contracts - - package-ethrex-guest runs-on: ubuntu-latest steps: - name: Checkout Code @@ -423,7 +439,10 @@ jobs: uses: actions/download-artifact@v6 with: path: ./bin - pattern: "ethrex*" # This includes the binaries, elf files, ethrex-verification-keys.tar.gz, and ethrex-contracts.tar.gz + # `*ethrex*`, not `ethrex*`: the stateless-validator artifacts are named + # stateless-validator-ethrex--, so a leading-anchor + # pattern would silently omit them from the release. + pattern: "*ethrex*" - name: Get previous tag run: | diff --git a/.gitignore b/.gitignore index 5764f85e57b..c748ecfb309 100644 --- a/.gitignore +++ b/.gitignore @@ -141,3 +141,6 @@ core.* __pycache__/ .lycheecache + +# Scratch output from .github/scripts/extract-stateless-fixture.sh +/output diff --git a/crates/guest-program/stateless-validator/bin/openvm/Cargo.toml b/crates/guest-program/stateless-validator/bin/openvm/Cargo.toml new file mode 100644 index 00000000000..773be4b8064 --- /dev/null +++ b/crates/guest-program/stateless-validator/bin/openvm/Cargo.toml @@ -0,0 +1,48 @@ +[workspace] + +[package] +version = "23.0.0" +name = "stateless-validator-ethrex-openvm" +edition = "2024" +license = "MIT OR Apache-2.0" + +[dependencies] +# openvm +openvm = { git = "https://github.com/openvm-org/openvm.git", tag = "v2.0.0", features = [ + "std", +] } +openvm-algebra-guest = { git = "https://github.com/openvm-org/openvm.git", tag = "v2.0.0" } +openvm-ecc-guest = { git = "https://github.com/openvm-org/openvm.git", tag = "v2.0.0" } + +# ere +ere-platform-openvm = { git = "https://github.com/eth-act/ere", rev = "a25f1aed9664c3b63e73ef05360090a4c41da31b", features = [ + "std", +] } + +ethrex-stateless-validator = { path = "../..", features = ["ere", "openvm"] } + +# Mirrors the ere-guests openvm bin: unifies the openvm source id across the +# bin, the crypto provider, and ere-platform-openvm. +[patch."https://github.com/openvm-org/openvm.git"] +openvm = { git = "https://github.com/openvm-org//openvm.git", tag = "v2.0.0" } +openvm-algebra-complex-macros = { git = "https://github.com/openvm-org//openvm.git", tag = "v2.0.0" } +openvm-algebra-guest = { git = "https://github.com/openvm-org//openvm.git", tag = "v2.0.0" } +openvm-algebra-moduli-macros = { git = "https://github.com/openvm-org//openvm.git", tag = "v2.0.0" } +openvm-custom-insn = { git = "https://github.com/openvm-org//openvm.git", tag = "v2.0.0" } +openvm-ecc-guest = { git = "https://github.com/openvm-org//openvm.git", tag = "v2.0.0" } +openvm-ecc-sw-macros = { git = "https://github.com/openvm-org//openvm.git", tag = "v2.0.0" } +openvm-k256 = { git = "https://github.com/openvm-org//openvm.git", tag = "v2.0.0", package = "k256" } +openvm-keccak256 = { git = "https://github.com/openvm-org//openvm.git", tag = "v2.0.0" } +openvm-keccak256-guest = { git = "https://github.com/openvm-org//openvm.git", tag = "v2.0.0" } +openvm-macros-common = { git = "https://github.com/openvm-org//openvm.git", tag = "v2.0.0" } +openvm-p256 = { git = "https://github.com/openvm-org//openvm.git", tag = "v2.0.0", package = "p256" } +openvm-pairing = { git = "https://github.com/openvm-org//openvm.git", tag = "v2.0.0" } +openvm-pairing-guest = { git = "https://github.com/openvm-org//openvm.git", tag = "v2.0.0" } +openvm-platform = { git = "https://github.com/openvm-org//openvm.git", tag = "v2.0.0" } +openvm-rv32im-guest = { git = "https://github.com/openvm-org//openvm.git", tag = "v2.0.0" } +openvm-sha2 = { git = "https://github.com/openvm-org//openvm.git", tag = "v2.0.0" } +openvm-sha2-guest = { git = "https://github.com/openvm-org//openvm.git", tag = "v2.0.0" } + +[profile.release] +codegen-units = 1 +lto = "fat" diff --git a/crates/guest-program/stateless-validator/bin/openvm/openvm_init.rs b/crates/guest-program/stateless-validator/bin/openvm/openvm_init.rs new file mode 100644 index 00000000000..257afb31e05 --- /dev/null +++ b/crates/guest-program/stateless-validator/bin/openvm/openvm_init.rs @@ -0,0 +1,4 @@ +// This file is automatically generated by cargo openvm. Do not rename or edit. +openvm_algebra_guest::moduli_macros::moduli_init! { "21888242871839275222246405745257275088696311157297823662689037894645226208583", "21888242871839275222246405745257275088548364400416034343698204186575808495617", "115792089237316195423570985008687907853269984665640564039457584007908834671663", "115792089237316195423570985008687907852837564279074904382605163141518161494337", "115792089210356248762697446949407573530086143415290314195533631308867097853951", "115792089210356248762697446949407573529996955224135760342422259061068512044369", "4002409555221667393417789825735904156556882819939007885332058136124031650490837864442687629129015664037894272559787", "52435875175126190479447740508185965837690552500527637822603658699938581184513" } +openvm_algebra_guest::complex_macros::complex_init! { "Bn254Fp2" { mod_idx = 0 }, "Bls12_381Fp2" { mod_idx = 6 } } +openvm_ecc_guest::sw_macros::sw_init! { "Bn254G1Affine", "Secp256k1Point", "P256Point", "Bls12_381G1Affine" } diff --git a/crates/guest-program/stateless-validator/bin/openvm/src/main.rs b/crates/guest-program/stateless-validator/bin/openvm/src/main.rs new file mode 100644 index 00000000000..dea599be0c1 --- /dev/null +++ b/crates/guest-program/stateless-validator/bin/openvm/src/main.rs @@ -0,0 +1,10 @@ +//! OpenVM Ethrex stateless validator guest program. + +use ere_platform_openvm::OpenVMPlatform; +use ethrex_stateless_validator::platform::entrypoint; + +openvm::init!(); + +fn main() { + entrypoint::(); +} diff --git a/crates/guest-program/stateless-validator/bin/sp1/Cargo.toml b/crates/guest-program/stateless-validator/bin/sp1/Cargo.toml new file mode 100644 index 00000000000..99f72b58447 --- /dev/null +++ b/crates/guest-program/stateless-validator/bin/sp1/Cargo.toml @@ -0,0 +1,27 @@ +[workspace] + +[package] +version = "23.0.0" +name = "stateless-validator-ethrex-sp1" +edition = "2024" +license = "MIT OR Apache-2.0" + +[dependencies] +ere-platform-sp1 = { git = "https://github.com/eth-act/ere", rev = "a25f1aed9664c3b63e73ef05360090a4c41da31b" } + +ethrex-stateless-validator = { path = "../..", features = ["ere", "sp1"] } + +# FIXME: Remove the patch once https://github.com/succinctlabs/sp1/pull/2865 is merged and released. +# (Mirrors the ere-guests sp1 bin: the fork exports the zkvm-standards IO symbols.) +[patch.crates-io] +sp1-lib = { git = "https://github.com/han0110/sp1", rev = "8564a70ea1952826fc0926eba5a3ce62af53ffac" } + +[patch."https://github.com/succinctlabs/sp1.git"] +sp1-lib = { git = "https://github.com/han0110/sp1", rev = "8564a70ea1952826fc0926eba5a3ce62af53ffac" } +sp1-primitives = { git = "https://github.com/han0110/sp1", rev = "8564a70ea1952826fc0926eba5a3ce62af53ffac" } +sp1-zkvm = { git = "https://github.com/han0110/sp1", rev = "8564a70ea1952826fc0926eba5a3ce62af53ffac" } +sp1-libzkevm = { git = "https://github.com/han0110/sp1", rev = "8564a70ea1952826fc0926eba5a3ce62af53ffac", package = "libzkevm" } + +[profile.release] +codegen-units = 1 +lto = "fat" diff --git a/crates/guest-program/stateless-validator/bin/sp1/src/main.rs b/crates/guest-program/stateless-validator/bin/sp1/src/main.rs new file mode 100644 index 00000000000..d65999a1155 --- /dev/null +++ b/crates/guest-program/stateless-validator/bin/sp1/src/main.rs @@ -0,0 +1,12 @@ +//! SP1 Ethrex stateless validator guest program. + +#![no_main] + +use ere_platform_sp1::{SP1Platform, sp1_zkvm}; +use ethrex_stateless_validator::platform::entrypoint; + +sp1_zkvm::entrypoint!(main); + +fn main() { + entrypoint::(); +} diff --git a/crates/guest-program/stateless-validator/bin/zisk/Cargo.toml b/crates/guest-program/stateless-validator/bin/zisk/Cargo.toml new file mode 100644 index 00000000000..b616381ac8a --- /dev/null +++ b/crates/guest-program/stateless-validator/bin/zisk/Cargo.toml @@ -0,0 +1,22 @@ +[workspace] + +[package] +version = "23.0.0" +name = "stateless-validator-ethrex-zisk" +edition = "2024" +license = "MIT OR Apache-2.0" + +[dependencies] +# NOTE: Using `default-features = false, features = ["inputcpy"]` mainly to disable the other +# feature enabled by default `user-hints`, which this binary doesn't use but introduces lots +# of dependencies. +ere-platform-zisk = { git = "https://github.com/eth-act/ere", rev = "a25f1aed9664c3b63e73ef05360090a4c41da31b", default-features = false, features = ["inputcpy"] } + +ethrex-stateless-validator = { path = "../..", features = ["ere", "zisk"] } + +[features] +cycle-scope = ["ere-platform-zisk/cycle-scope"] + +[profile.release] +codegen-units = 1 +lto = "fat" diff --git a/crates/guest-program/stateless-validator/bin/zisk/src/main.rs b/crates/guest-program/stateless-validator/bin/zisk/src/main.rs new file mode 100644 index 00000000000..d28391a98a6 --- /dev/null +++ b/crates/guest-program/stateless-validator/bin/zisk/src/main.rs @@ -0,0 +1,12 @@ +//! ZisK Ethrex stateless validator guest program. + +#![no_main] + +use ere_platform_zisk::{ZiskPlatform, ziskos}; +use ethrex_stateless_validator::platform::entrypoint; + +ziskos::entrypoint!(main); + +fn main() { + entrypoint::(); +} From f412ef2de48f654a94e9756e7f0b72a2eee12fb1 Mon Sep 17 00:00:00 2001 From: Lucas Fiegl Date: Wed, 5 Aug 2026 10:26:58 -0300 Subject: [PATCH 16/30] docs(l1): rewrite eip-8025 for the stateless guest --- docs/eip-8025-zkboost-testnet.md | 2 +- docs/eip-8025.md | 776 +++++++++---------------------- 2 files changed, 208 insertions(+), 570 deletions(-) diff --git a/docs/eip-8025-zkboost-testnet.md b/docs/eip-8025-zkboost-testnet.md index ed4e1e166af..bc86fb87b33 100644 --- a/docs/eip-8025-zkboost-testnet.md +++ b/docs/eip-8025-zkboost-testnet.md @@ -43,7 +43,7 @@ cd lighthouse && git checkout feat/eip8025 ### Build ```bash -# ethrex (EIP-8025 host code is always-compiled; eip-8025 enables SSZ guest twins) +# ethrex (stateless validation is unconditional; there is no feature flag) cd ethrex cargo build --release --bin ethrex diff --git a/docs/eip-8025.md b/docs/eip-8025.md index 89da8f86942..6ce3c1194b9 100644 --- a/docs/eip-8025.md +++ b/docs/eip-8025.md @@ -2,632 +2,270 @@ ## Overview -EIP-8025 enables beacon nodes to verify execution payload validity without re-executing transactions, using zkEVM proofs instead. ethrex integrates via the **zkboost sidecar** pattern: the node exposes `debug_executionWitness` and `debug_chainConfig` RPC endpoints, and an external prover (zkboost) fetches witnesses, generates proofs, and submits them back. - -> **Note**: An earlier iteration used internal Engine API proof endpoints and a built-in proof coordinator. Those were removed in favor of the zkboost sidecar approach, which decouples proving from consensus. -> -> **Stale sections below**: the "Architecture", "Crate Layout", "Engine API Endpoints", "CLI Flags", "Implementation Status", and "Scope" sections that follow describe that removed internal-proof design (they reference `crates/blockchain/proof_coordinator/`, `crates/networking/rpc/engine/proof.rs`, `engine_requestProofsV1`/`engine_verifyExecutionProofV1`, and `--proof-callback.url` / `--proof-coordinator.*` on the L1 node — none of which exist in the current code). They are retained for historical context only. The live integration is the zkboost sidecar over `debug_executionWitness` / `debug_chainConfig` described above. - -ethrex is uniquely positioned for this: it already has a complete witness generation pipeline, guest programs for stateless re-execution inside zkVMs (SP1, RISC0, ZisK, OpenVM), and a distributed proving infrastructure for L2. This change brings those capabilities to L1. +EIP-8025 lets beacon nodes verify execution payload validity without re-executing +transactions, using zkEVM proofs instead. ethrex integrates via the **zkboost +sidecar** pattern: the node exposes `debug_executionWitness` and +`debug_chainConfig`, and an external prover fetches witnesses, generates proofs, +and submits them back. + +ethrex owns the stateless-validator guest program and publishes its compiled ELFs +and verification keys as release assets, so `eth-act/ere-guests` can consume them +rather than building its own ethrex guest (issue +[#7011](https://github.com/lambdaclass/ethrex/issues/7011)). + +### Target schema + +The wire format tracks **execution-specs master at commit +`3c3b6f4af315b268a61e20d5a4da8aa4f24c91f0`**, which is two merged PRs: + +| PR | merged | change | +|---|---|---| +| [#3248](https://github.com/ethereum/execution-specs/pull/3248) | 2026-07-29 | "change StatelessInput SSZ serialization to be EIP-7688 aligned" — introduces `ProgressiveContainer` / `ProgressiveList` | +| [#3278](https://github.com/ethereum/execution-specs/pull/3278) | 2026-08-03 | deletes `ChainConfig` from the wire, adds `schema_id` to the output | + +Neither has shipped in a `tests-zkevm` release — the newest is `v0.6.2` +(2026-07-13), which predates both. Conformance vectors are therefore **generated** +from the pinned spec commit; see [Testing](#testing). + +> **Schema id `0x1501` is overloaded.** Upstream changed the wire across those two +> PRs without bumping the revision byte, so `0x1501` means the pre-#3248 body in +> `v0.6.2` fixtures and the current body on master. A consumer cannot tell them +> apart from the id alone. ethrex speaks the newer dialect; anything still on the +> older one — including `ere-guests`' `stateless-validator-common` as of +> 2026-08-05 — will misparse our artifacts until it moves. ### References | Resource | Link | |----------|------| | EIP-8025 | [eips.ethereum.org/EIPS/eip-8025](https://eips.ethereum.org/EIPS/eip-8025) | -| Engine API spec | [execution-apis PR #735](https://github.com/ethereum/execution-apis/pull/735) | -| Consensus specs | [consensus-specs PR #4828](https://github.com/ethereum/consensus-specs/pull/4828) | -| Beacon API spec | [beacon-APIs PR #569](https://github.com/ethereum/beacon-APIs/pull/569) | -| ere-guests design | [ere-guests PR #7](https://github.com/eth-act/ere-guests/pull/7) | +| Stateless spec | [`stateless_ssz.py`](https://github.com/ethereum/execution-specs/blob/3c3b6f4af315b268a61e20d5a4da8aa4f24c91f0/src/ethereum/forks/amsterdam/stateless_ssz.py) | +| Guest entrypoint | [`stateless_guest.py`](https://github.com/ethereum/execution-specs/blob/3c3b6f4af315b268a61e20d5a4da8aa4f24c91f0/src/ethereum/forks/amsterdam/stateless_guest.py) | +| EIP-7916 (progressive lists) | [eips.ethereum.org/EIPS/eip-7916](https://eips.ethereum.org/EIPS/eip-7916) | | libssz | [github.com/lambdaclass/libssz](https://github.com/lambdaclass/libssz) | +| Ere / ere-guests | [eth-act/ere](https://github.com/eth-act/ere), [eth-act/ere-guests](https://github.com/eth-act/ere-guests) | --- ## Architecture ``` -┌────────────────────────────────────────────────────────────────────────────┐ -│ ethrex node (always-compiled; eip-8025 feature enables SSZ guest twins) │ -│ │ -│ ┌──────────────────────────────────────────────────────────────────────┐ │ -│ │ Engine API (crates/networking/rpc/engine/proof.rs) │ │ -│ │ │ │ -│ │ ├─ engine_requestProofsV1 → RequestProofsV1::handle() │ │ -│ │ ├─ engine_verifyExecutionProofV1 → VerifyExecutionProofV1::handle() │ │ -│ │ └─ engine_verifyNewPayloadReq → VerifyNewPayloadRequestHeaderV1 │ │ -│ │ HeaderV1 ::handle() │ │ -│ └─────────┬─────────────────────────────────────┬──────────────────────┘ │ -│ │ │ │ -│ ▼ ▼ │ -│ ┌──────────────────┐ ┌──────────────────────────────────────────┐ │ -│ │ Blockchain │ │ L1 ProofCoordinator (GenServer, TCP) │ │ -│ │ │ │ (crates/blockchain/proof_coordinator/) │ │ -│ │ • generate_ │ │ │ │ -│ │ witness_for_ │ │ RPC handler: │ │ -│ │ blocks() │ │ 1. payload → Block │ │ -│ │ │ │ 2. generate witness │ │ -│ └──────────────────┘ │ 3. send Block + witness to coordinator│ │ -│ │ │ │ -│ │ Coordinator (on prover connect): │ │ -│ │ 4. build ProgramInput from Block + │ │ -│ │ witness │ │ -│ │ 5. dispatch to prover │ │ -│ │ │ │ -│ │ verify_proof (RPC handler): │ │ -│ │ 1. store proof in EXECUTION_PROOFS │ │ -│ │ │ │ -│ │ verify_header (RPC handler): │ │ -│ │ 1. compute SSZ root from header │ │ -│ │ 2. lookup EXECUTION_PROOFS │ │ -│ │ 3. check >= MIN_REQUIRED (=1) │ │ -│ └─────────────────────┬────────────────────┘ │ -│ │ │ -│ ┌───────────────────────────────────────────────▼──────────────────────┐ │ -│ │ Prover dispatch (pull model): │ │ -│ │ • Prover connects with InputRequest { prover_type } │ │ -│ │ • Coordinator builds ProgramInput and sends it │ │ -│ │ • Prover proves, sends ProofSubmit { ProverOutput } back │ │ -│ │ • Coordinator stores proof in EXECUTION_PROOFS │ │ -│ │ • Coordinator POSTs GeneratedProof to callback_url (if configured) │ │ -│ └──────────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────────────────┐ │ -│ │ Store — EXECUTION_PROOFS table (128-block retention) │ │ -│ └──────────────────────────────────────────────────────────────────────┘ │ -└────────────────────────────────────────────────────────────────────────────┘ - - Provers connect to coordinator (pull model) - ┌────────────────┬────────────────┐ - ▼ ▼ ▼ - ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ - │ SP1 Prover │ │ RISC0 Prover │ │ ZisK Prover │ - │ (any machine)│ │ (any machine)│ │ (any machine)│ - │ Pulls input │ │ Pulls input │ │ Pulls input │ - │ Proves │ │ Proves │ │ Proves │ - │ Returns proof│ │ Returns proof│ │ Returns proof│ - └──────────────┘ └──────────────┘ └──────────────┘ -``` - -### Crate Layout - -| Crate | Path | Purpose | -|-------|------|---------| -| `ethrex-common` | `crates/common/types/stateless_ssz.rs` | SSZ containers (`NewPayloadRequest`, `ExecutionPayload`, headers) with `hash_tree_root` | -| `ethrex-common` | `crates/common/types/prover.rs` | Shared prover types: `ProofBytes`, `ProverOutput`, `ProofData` protocol | -| `ethrex-blockchain` | `crates/blockchain/proof_coordinator/` | L1 ProofCoordinator (GenServer, TCP, pull model), types, config | -| `ethrex-rpc` | `crates/networking/rpc/engine/proof.rs` | Engine API RPC handlers for the three endpoints | -| `ethrex-prover` | `crates/prover/` | Shared prover infrastructure (backends, `Prover` GenServer pull loop) | -| `ethrex-guest-program` | `crates/guest-program/src/l1/` | Modified guest program with EIP-8025 input/output format | -| `ethrex-storage` | `crates/storage/` | `EXECUTION_PROOFS` table with 128-block retention | - ---- - -## Engine API Endpoints - -### `engine_requestProofsV1` - -Initiates proof generation for an execution payload. Returns immediately with a `ProofGenId`. - -``` -Method: engine_requestProofsV1 - -Params: - [0] executionPayload — ExecutionPayloadV3 (same as engine_newPayloadV3) - [1] versionedHashes — Array - [2] parentBeaconBlockRoot — DATA(32) - [3] executionRequests — Array - [4] proofAttributes — { proofTypes: Array } - -Returns: DATA(8) — ProofGenId - -Errors: - -39003: Invalid payload - -39004: Proof generation unavailable - -Timeout: 1s -``` - -**Example request:** -```json -{ - "jsonrpc": "2.0", - "method": "engine_requestProofsV1", - "params": [ - { - "parentHash": "0xabc...", - "feeRecipient": "0x...", - "stateRoot": "0x...", - "receiptsRoot": "0x...", - "logsBloom": "0x...", - "prevRandao": "0x...", - "blockNumber": "0x1", - "gasLimit": "0x1c9c380", - "gasUsed": "0x5208", - "timestamp": "0x60000000", - "extraData": "0x", - "baseFeePerGas": "0x7", - "blockHash": "0x...", - "transactions": ["0x..."], - "withdrawals": [], - "blobGasUsed": "0x0", - "excessBlobGas": "0x0" - }, - [], - "0x0000000000000000000000000000000000000000000000000000000000000000", - [], - { "proofTypes": ["0x1", "0x2"] } - ], - "id": 1 -} -``` - -**Example response:** -```json -{ - "jsonrpc": "2.0", - "result": "0x00000001abcdef01", - "id": 1 -} -``` - -### `engine_verifyExecutionProofV1` - -Verifies a submitted execution proof and stores it if valid. - -``` -Method: engine_verifyExecutionProofV1 - -Params: - [0] ExecutionProofV1: - proofData — DATA (max 300 KiB) - proofType — QUANTITY(u64) - publicInput — { newPayloadRequestRoot: DATA(32) } - -Returns: ProofStatusV1 { status: "VALID"|"INVALID"|"NOT_SUPPORTED", error? } - -Errors: - -39001: Invalid proof format - -Timeout: 1s -``` - -**Example request:** -```json -{ - "jsonrpc": "2.0", - "method": "engine_verifyExecutionProofV1", - "params": [{ - "proofData": "0xdeadbeef...", - "proofType": "0x1", - "publicInput": { - "newPayloadRequestRoot": "0x1234567890abcdef..." - } - }], - "id": 2 -} -``` - -**Example response:** -```json -{ - "jsonrpc": "2.0", - "result": { "status": "VALID" }, - "id": 2 -} -``` - -### `engine_verifyNewPayloadRequestHeaderV1` - -Checks whether enough valid proofs exist for a given payload header. - -``` -Method: engine_verifyNewPayloadRequestHeaderV1 - -Params: - [0] NewPayloadRequestHeaderV1: - executionPayloadHeader — headerized payload (roots instead of lists) - versionedHashes — Array - parentBeaconBlockRoot — DATA(32) - executionRequests — Array - -Returns: ProofStatusV1 { status: "VALID"|"SYNCING", error? } - -Errors: - -39002: Invalid header format - -Timeout: 1s -``` - -**Flow:** Computes the SSZ `hash_tree_root` of the `NewPayloadRequestHeader`, looks up stored proofs by that root, and returns `VALID` if at least `MIN_REQUIRED_EXECUTION_PROOFS` (1) proofs exist. - ---- - -## End-to-End Data Flow - + statelessInputBytes + │ + ┌──────────────────┴───────────────────┐ + │ │ + EXECUTE precompile zkVM guest ELF + (native rollups, L1) (ZisK / SP1 / OpenVM) + │ │ + blockchain/stateless.rs stateless-validator/bin/ + │ │ + └──────────────┬───────────────────────┘ + ▼ + ethrex_guest_program::l1::run_stateless_guest + │ + decode prefix → rebuild witness → check public keys + → reconstruct block → execute → verify block_hash + │ + ▼ + statelessOutputBytes ``` -Beacon Node (BN) ethrex (EL) Prover Workers - │ │ │ - │ engine_requestProofsV1 │ │ - │ { payload, hashes, │ │ - │ beacon_root, requests, │ │ - │ proof_attributes } │ │ - │───────────────────────────────▶│ │ - │ │ 1. Convert payload → Block │ - │ │ 2. Generate ExecutionWitness │ - │ ProofGenId │ 3. Send Block + witness to │ - │◀───────────────────────────────│ coordinator (AddRequest) │ - │ │ │ - │ │ L1 ProofCoordinator │ - │ │◀──────────────────────────────│ Worker connects - │ │ "I'm SP1, give me work" │ (InputRequest) - │ │ 4. Build ProgramInput from │ - │ │ Block + witness │ - │ │──────────────────────────────▶│ ProgramInput - │ │ │ - │ │ │ 5. Prove inside - │ │ │ zkVM - │ │ │ - │ │◀──────────────────────────────│ ProofSubmit - │ │ │ { ProverOutput } - │ │ 6. Store in EXECUTION_PROOFS │ - │ POST /eth/v1/prover/ │ 7. Build GeneratedProof │ - │ execution_proofs │ { proof_gen_id, │ - │◀───────────────────────────────│ execution_proof } │ - │ │ 8. POST to callback_url │ - │ │ │ - │ (prover signs, gossips) │ │ - │ │ │ - │ engine_verifyExecutionProofV1 │ │ - │───────────────────────────────▶│ 9. Store in EXECUTION_PROOFS │ - │ { status: "VALID" } │ │ - │◀───────────────────────────────│ │ - │ │ │ - │ engine_verifyNewPayloadReqHdr │ │ - │───────────────────────────────▶│ 10. Compute SSZ root │ - │ │ 11. Lookup proofs ≥ 1 │ - │ { status: "VALID" } │ │ - │◀───────────────────────────────│ │ -``` - ---- - -## Key Design Decisions -### 1. Distributed Proving: Pull Model +Both consumers share one implementation. That is deliberate: while they were +separate, the per-transaction public-key check existed on only one of them, and the +EXECUTE path silently never performed it. -Provers **pull** work from the coordinator (same pattern as L2): +### Wire format -- Coordinator holds `ProgramInput` (witness generated **once** by the node) -- Multiple provers with different backends connect and request work -- Each prover independently proves the same input with its own zkVM -- Provers submit proof back; coordinator stores it and delivers via callback - -This avoids GPU contention, redundant witness generation, and the coordinator needing to know prover URLs. - -### 2. Separate L1 ProofCoordinator - -The L2 `ProofCoordinator` is deeply coupled to L2 concerns (`StoreRollup`, `EthClient`, aligned mode, TDX, batch numbering). Instead of attempting to generalize it, we write a new L1-specific coordinator using the same `spawned_concurrency::GenServer` framework: - -```rust -pub struct L1ProofCoordinator { - store: Store, - config: ProofCoordinatorConfig, - listener: Option>, - pending: HashMap, - http_client: reqwest::Client, -} - -impl GenServer for L1ProofCoordinator { - type CastMsg = CoordCastMsg; // AcceptNext, AddRequest - // Accept TCP connections, dispatch work, receive proofs -} ``` +input: [BE u16 schema_id = 0x1501] ++ SSZ(SszStatelessInput) -The RPC handler sends `Block` + `ExecutionWitness` to the coordinator via `AddRequest`. The coordinator builds `ProgramInput` only when a prover connects and requests work, keeping the RPC layer free of guest program dependencies. - -### 3. Shared Prover Infrastructure - -Extracted from `crates/l2/prover/` into `crates/prover/` (shared by L1 and L2): - -| Component | Shared Location | -|-----------|-----------------| -| `ProverBackend` trait | `crates/prover/src/backend/mod.rs` | -| SP1/RISC0/ZisK/OpenVM backends | `crates/prover/src/backend/*.rs` | -| `Prover` (GenServer pull loop) | `crates/prover/src/prover.rs` | -| `ProofData` protocol | `crates/common/types/prover.rs` (re-exported via `crates/prover/src/protocol.rs`) | - -The `Prover` is generic over the input type `I`: -- **L2**: `Prover` where `ProverInputData: Into` -- **L1**: `Prover` where `Into` is identity - -#### Proof output types - -Backends produce a `ProverOutput` enum with two variants, mirroring the old `BatchProof` design: - -```rust -pub struct ProofBytes { - pub prover_type: ProverType, - pub proof: Vec, -} - -pub enum ProverOutput { - /// Just the proof — for L1 and L2 on-chain verification. - Proof(ProofBytes), - /// Proof + public values — for Aligned Layer verification. - ProofWithPublicValues { proof_bytes: ProofBytes, public_values: Vec }, +SszStatelessInput { + new_payload_request: SszNewPayloadRequest, + witness: SszExecutionWitness, + chain_id: uint64, + public_keys: SszList[ByteVector[65], 2**20], } -``` - -- `Groth16` format → `ProverOutput::Proof(...)` (on-chain, no public values needed) -- `Compressed` format → `ProverOutput::ProofWithPublicValues { ... }` (Aligned, needs public values) -- `ProofBytes` is the minimal default type; public values only appear where needed - -### 4. SSZ Types via libssz - -SSZ containers for `hash_tree_root` computation live in `ethrex-common` (not `ethrex-blockchain`) because guest programs can't depend on heavy crates (tokio, rocksdb): -```rust -// crates/common/types/stateless_ssz.rs +output: SSZ(SszStatelessValidationResult) // 43 bytes, entirely fixed-size -#[derive(SszEncode, SszDecode, HashTreeRoot)] -pub struct NewPayloadRequest { - pub execution_payload: ExecutionPayload, - pub versioned_hashes: SszList<[u8; 32], 4096>, - pub parent_beacon_block_root: [u8; 32], - pub execution_requests: ExecutionRequests, +SszStatelessValidationResult { + new_payload_request_root: Bytes32, // @0 + successful_validation: boolean, // @32 + chain_id: uint64, // @33, LE + schema_id: uint16, // @41, LE } - -// The hash_tree_root of this container is the public input -// that execution proofs commit to. -impl NewPayloadRequest { - pub fn public_input(&self) -> PublicInput { - PublicInput { - new_payload_request_root: self.hash_tree_root(), - } - } -} -``` - -### 5. Guest Program Modification - -Aligned with the [ere-guests](https://github.com/eth-act/ere-guests/pull/7) design: - -**Standard mode** (without `eip-8025` feature): -``` -Input: ProgramInput { blocks: Vec, execution_witness } -Output: ProgramOutput { initial_state_hash, final_state_hash, last_block_hash, chain_id, tx_count } - → 160 bytes -``` - -**EIP-8025 mode** (with `eip-8025` feature): -``` -Input: (NewPayloadRequest [SSZ], ExecutionWitness [rkyv]) -Output: ProgramOutput { new_payload_request_root: [u8; 32], valid: bool, chain_id: u64 } - → 41 bytes: 32-byte root + 1-byte boolean + 8-byte chain_id ``` -The EIP-8025 guest program: -1. Receives `NewPayloadRequest` + `ExecutionWitness` in a length-prefixed wire format -2. Computes `hash_tree_root(NewPayloadRequest)` using SSZ/SHA256 -3. Converts `NewPayloadRequest` → EL `Block` internally -4. Validates `block_hash` and `versioned_blob_hashes` -5. Executes the block statelessly -6. Returns `(root, true)` on success or `(root, false)` on failure - -**Wire format:** `[ssz_len: u32 LE] [ssz_bytes] [rkyv_bytes]` - -### 6. Persistent ProofStore - -New `EXECUTION_PROOFS` table in Store, following the same pattern as `EXECUTION_WITNESSES`: - -| Property | Value | -|----------|-------| -| Key | `(block_number: u64, root: H256, proof_type: u64)` — 48 bytes | -| Value | Serialized proof data | -| Retention | 128 blocks (same as witnesses) | -| Max per payload | 4 (`MAX_EXECUTION_PROOFS_PER_PAYLOAD`) | -| Cleanup | Automatic on each `store_execution_proof()` call | - ---- - -## Constants - -From the consensus specs ([PR #4828](https://github.com/ethereum/consensus-specs/pull/4828)): - -| Name | Value | Description | -|------|-------|-------------| -| `MIN_REQUIRED_EXECUTION_PROOFS` | `1` | Minimum valid proofs for `verify_header` to return VALID | -| `MAX_PROOF_SIZE` | `307200` (300 KiB) | Maximum `proofData` size per proof | -| `MAX_EXECUTION_PROOFS_PER_PAYLOAD` | `4` | Maximum proofs stored per payload | +The schema id is the **only** carrier of the fork: #3278 removed all chain +configuration from the body, so rejecting an unexpected id is a correctness +requirement rather than a sanity check. It is also echoed as a public output so a +verifier can pin which fork rules were applied — necessary because forks such as +Osaka and the BPOs share a payload shape. + +A decode failure commits the all-zero default. A decodable input commits the real +root, `chain_id` and `schema_id` **even when validation fails**; zero sentinels +signal decode failure only, and the root is computed before validation runs. + +### Chain configuration + +There is no chain config on the wire. `amsterdam_chain_config(chain_id)` derives it +from `(chain_id, fork)`, with the fork fixed by the schema id: every fork up to and +including Amsterdam active at 0, Amsterdam blob parameters, and **no +payload-timestamp-versus-activation check**. + +That mirrors EEST, which ships one implementation per fork and therefore skips the +check — and matching it is what keeps ethrex byte-identical to the generated +vectors. The spec's `verify_stateless_new_payload` nonetheless comments that "a real +implementation MUST do these checks" while providing no activation data to check +against; the gap is recorded as a `TODO(upstream)` in +`crates/common/types/block_execution_witness.rs`. + +### Crate layout + +| Path | Purpose | +|---|---| +| `crates/common/types/stateless_ssz.rs` | the SSZ containers, shared by both consumers | +| `crates/guest-program/src/l1/` | schema-prefix codec, `run_stateless_guest`, validation | +| `crates/guest-program/stateless-validator/` | own cargo workspace: per-zkVM `Crypto` providers, ere-platform entrypoint | +| `crates/guest-program/stateless-validator/bin/{zisk,sp1,openvm}/` | the guest binaries | +| `crates/blockchain/stateless.rs` | the EXECUTE precompile's entry into the shared path | +| `crates/prover/` | host backends; `ExecBackend` runs the same entrypoint natively | + +The stateless-validator workspace pins its own zkVM SDK versions, matching what +`ere`'s catalog resolves. That isolation matters: the L2 batch guest stays on SP1 +5.0.8 and OpenVM 1.4.1, so bumping the stateless guest cannot move L2 verification +keys or force a contract redeployment. + +### Progressive SSZ + +Since #3248, `SszExecutionPayload` is a `ProgressiveContainer(active_fields=[1;19])` +and `SszExecutionRequests` is `[1;5]`; `transactions`, `withdrawals`, +`versioned_hashes` and `block_access_list` are progressive lists. Nothing +progressive appears on the wire — `ProgressiveList` delegates encode and decode to +`Vec` — so only merkleization changes, and the SSZ byte offsets are unaffected. + +`libssz-derive` has no progressive support, so `HashTreeRoot` is hand-written for +exactly those two containers as +`mix_in_active_fields(merkleize_progressive(field_roots), &[true; N])`. + +> **Known upstream defect.** `libssz-merkle 0.2.2`'s `merkleize_progressive` +> reverses the subtree children relative to the reference implementation, so every +> progressive root it produces is wrong. +> `test/tests/common/progressive_ssz_tests.rs` pins the correct roots against +> remerkleable and carries the derivation; both cases are `#[ignore]`d until the +> one-line fix lands. Measured against the 769-block vector set on 2026-08-05: 8 +> exact matches, **755 differing only in the 32-byte root**, 6 differing more +> widely. On those 755, `successful_validation`, `chain_id` and `schema_id` are +> already byte-identical to the reference — including on true-success cases — so +> everything except that one hash function agrees with execution-specs today. --- -## Configuration & Deployment - -### CLI Flags +## Guest crypto -All flags are part of the always-compiled node configuration: +Each zkVM gets a `Crypto` implementation, selected by cargo feature: -| Flag | Default | Description | -|------|---------|-------------| -| `--proof-callback.url` | None | URL to POST `GeneratedProof` payloads (Beacon API) | -| `--proof-coordinator.addr` | `127.0.0.1` | Bind address for ProofCoordinator TCP server | -| `--proof-coordinator.port` | `9100` | Port for ProofCoordinator TCP server | +| Target | Provider | +|---|---| +| ZisK, SP1 | `zkvm-interface` — eth-act's `zkvm-standards` syscalls | +| OpenVM | OpenVM guest libraries (`openvm-k256`, `openvm-pairing`, `openvm-kzg`, …) | +| host | `NativeCrypto` | -### Example: Multi-Prover Deployment - -```bash -# Start the ethrex node with proof generation enabled -ethrex \ - --proof-callback.url http://beacon:5052/eth/v1/prover/execution_proofs \ - --proof-coordinator.addr 0.0.0.0 \ - --proof-coordinator.port 9100 - -# Start prover workers on separate machines (each with GPU) -ethrex-prover --backend sp1 --coordinator http://ethrex-node:9100 -ethrex-prover --backend risc0 --coordinator http://ethrex-node:9100 -ethrex-prover --backend zisk --coordinator http://ethrex-node:9100 -``` - -### Example: Single-Prover Deployment - -```bash -# Simplest setup: one node, one prover on the same machine -ethrex \ - --proof-callback.url http://beacon:5052/eth/v1/prover/execution_proofs \ - --proof-coordinator.addr 0.0.0.0 \ - --proof-coordinator.port 9100 - -ethrex-prover --backend sp1 --coordinator http://localhost:9100 -``` +Routing ZisK and SP1 through zkvm-standards syscalls, rather than the `sp1-patches` +crate stack, is what lets the SP1 guest build with **no `sp1-patches` tags at +all** — no `sha2`, `k256`, `secp256k1`, `substrate-bn` or `crypto-bigint` patch pins +to keep in step with the SDK. --- -## Feature Gating +## Released artifacts -The EIP-8025 host-side code that still exists — the SSZ stateless types and the -stateless-validation pipeline used by the native-rollup EXECUTE precompile — is -**always-compiled** into the node binary; the old unified devnet feature flag was -removed. (The internal Engine API proof endpoints and built-in proof coordinator -that earlier sections of this document describe were removed in favor of the -zkboost sidecar — see the Overview note. They are no longer compiled into the -binary at all.) The only remaining cargo feature is `eip-8025`, which controls -the **guest program** input/output format: +`tag_release.yaml` builds, keygens, verifies and attaches, per zkVM: ``` -crates/guest-program (eip-8025) - └─ ethrex-common/eip-8025, ethrex-vm/eip-8025 - └─ dep:libssz, dep:libssz-merkle, dep:libssz-types, dep:libssz-derive - -crates/blockchain (eip-8025) - └─ ethrex-common/eip-8025, ethrex-vm/eip-8025 +stateless-validator-ethrex-zisk-1.0.0-alpha.elf + .vk +stateless-validator-ethrex-sp1-6.3.1.elf + .vk +stateless-validator-ethrex-openvm-2.0.0.elf + .vk ``` -When `eip-8025` is compiled into the guest: -- Guest receives `NewPayloadRequest` (SSZ) + `ExecutionWitness` (rkyv) on the wire -- Guest returns `(new_payload_request_root, valid, chain_id)` — the 41-byte - EIP-8025 output (32-byte root + 1-byte bool + 8-byte chain_id LE) +Compilation runs in `ghcr.io/eth-act/ere/ere-compiler-`, and `.vk` comes from +`ere-server … keygen`, so the artifacts are produced by the same toolchain that will +consume them. Version strings come from `.github/scripts/zkvm-version.sh`, which +refuses to answer if the pinned `ere` rev in a bin manifest has moved — a rev bump +cannot silently mislabel an asset. -Without `eip-8025` in the guest: -- Standard rkyv `ProgramInput`/`ProgramOutput` format preserved -- Host always has the SSZ types compiled in (for the EXECUTE precompile) +`verify-stateless-validator-guest` then runs each freshly built ELF under +`ere-server … execute` against a generated conformance vector and requires the +returned `statelessOutputBytes` to match byte-for-byte. It deliberately uses a +**true-success** vector: the root, `chain_id` and `schema_id` are all computed +before or without executing the block, so a guest whose execution is broken +reproduces a failure vector exactly. Only a success case proves the ELF validates. --- -## Testing: zkevm Execution-Spec-Tests - -### Approach - -The `tests-zkevm@v0.5.0` release from [ethereum/execution-specs](https://github.com/ethereum/execution-specs) provides ~12,000 Amsterdam-fork fixtures, each with a pre-filled `executionWitness` in the standard format (flat MPT nodes, bytecodes, ancestor headers). This tests the real EIP-8025 scenario: a consensus layer provides a witness, and our execution layer validates the block with it. - -### Two Test Paths - -| Path | Witness Source | What It Proves | -|------|---------------|----------------| -| `re_run_stateless` (existing) | Generated from blockchain execution | Internal consistency | -| `run_stateless_from_fixture` (new) | Provided by zkevm fixture | Interop with external witnesses | - -### Conversion Pipeline - -``` -zkevm fixture JSON - ├── executionWitness.state ─┐ - ├── executionWitness.codes ─┼──▶ RpcExecutionWitness (standard format) - └── executionWitness.headers ─┘ - │ - ▼ - execution_witness_from_rpc_chain_config() - │ - ▼ - ExecutionWitness (internal, structured tries) - │ - ▼ - ProgramInput { blocks, execution_witness } - │ - ▼ - execution_program() - │ - ▼ - ProgramOutput ──compare──▶ statelessOutputBytes (from fixture) -``` - -### Running the Tests +## Testing ```bash -# Download zkevm fixtures -cd tooling/ef_tests/blockchain -make zkevm-vectors +# Generate conformance vectors from the pinned execution-specs commit. +# Needs `uv`; clones the spec and fills its EIP-8025 tests. +make -C tooling/ef_tests/blockchain stateless-vector -# Run all blockchain EF tests (includes zkevm Amsterdam fixtures) -make test +# Blockchain EF tests, including the stateless comparison. +make -C tooling/ef_tests/blockchain test-stateless -# Run stateless tests with zkevm fixtures -make test-stateless-zkevm +# Guest-crate host equivalence over the same vectors. +export ETHREX_STATELESS_FIXTURES=$PWD/tooling/ef_tests/blockchain/vectors_stateless_3278/blockchain_tests +cargo test --manifest-path crates/guest-program/stateless-validator/Cargo.toml --features host,ere ``` -### zkevm Amsterdam Fork Compatibility - -The `tests-zkevm@v0.5.0` Amsterdam fork definition differs from ethrex's: - -| EIP | zkevm Amsterdam | ethrex Amsterdam | -|-----|-----------------|-----------------| -| EIP-7928 (Block Access Lists) | Yes | Yes | -| EIP-7708 (ETH Transfer Logs) | No | Yes | -| EIP-7778 (Block Gas Without Refunds) | No | Yes | -| EIP-7843 (SLOTNUM opcode) | No | Yes | +The vector set is generated rather than downloaded because no `tests-zkevm` release +carries this schema. `gen_stateless_vectors.sh` asserts that every output is exactly +43 bytes and that at least one case is a true success, so a silently pre-#3278 +checkout or an oracle that cannot prove execution both fail loudly. Replace all of +this with `tests-zkevm@v0.7.x` once it exists. -To handle this, zkevm tests run with an Osaka-level config (`ZKEVM_AMSTERDAM_CONFIG`) that excludes the unsupported EIPs, plus supplementary validation for edge cases. See the `TODO(zkevm)` comment in `tooling/ef_tests/blockchain/fork.rs` for the cleanup plan when zkevm adds support for these EIPs. +Two host-side tests guard the guest: `host_fixtures.rs` compares output bytes +against the vectors, and `platform_parity.rs` asserts the ere-platform path and the +plain runner agree byte-for-byte, so the cycle-scope instrumentation cannot change +behaviour. --- -## Spec Notes +## Spec notes -The Engine API spec ([PR #735](https://github.com/ethereum/execution-apis/pull/735)) has internal inconsistencies. We follow the **markdown** version: +The Engine API spec ([PR #735](https://github.com/ethereum/execution-apis/pull/735)) +has internal inconsistencies; we follow the **markdown** version: -1. **`ProofStatusV1`**: Markdown defines `status` as a string enum (`VALID`/`INVALID`/`SYNCING`/`NOT_SUPPORTED`). The OpenRPC schema defines it as `{valid: boolean}`. We use the string enum — a boolean can't express SYNCING or NOT_SUPPORTED. - -2. **`engine_requestProofsV1` params**: Markdown defines 5 params (including `executionRequests`). The OpenRPC schema lists 4 (omitting it). We use 5 — `executionRequests` is essential to build `NewPayloadRequest`. - -3. **`proofType` width**: Engine API uses `QUANTITY, 64 Bits` (u64). Beacon API SSZ uses `Uint8`. We use u64 in the RPC layer to match the Engine API spec. +1. **`ProofStatusV1`** — markdown defines `status` as a string enum + (`VALID`/`INVALID`/`SYNCING`/`NOT_SUPPORTED`), the OpenRPC schema as + `{valid: boolean}`. We use the string enum; a boolean cannot express SYNCING or + NOT_SUPPORTED. +2. **`engine_requestProofsV1` params** — markdown defines 5 (including + `executionRequests`), OpenRPC lists 4. We use 5; `executionRequests` is needed to + build `NewPayloadRequest`. +3. **`proofType` width** — Engine API uses `QUANTITY, 64 Bits`; Beacon API SSZ uses + `Uint8`. We use u64 in the RPC layer. --- ## Scope -### In Scope - -- Engine API: `engine_requestProofsV1`, `engine_verifyExecutionProofV1`, `engine_verifyNewPayloadRequestHeaderV1` -- L1 ProofCoordinator in `crates/blockchain/proof_coordinator/` -- Persistent `EXECUTION_PROOFS` table (128-block retention) -- L1 ProofCoordinator for distributed proving (pull model) -- Callback delivery: HTTP POST to Beacon API -- Guest program modification for EIP-8025 public input format -- SSZ types via libssz for `hash_tree_root` computation -- Shared prover infrastructure extracted from `crates/l2/prover/` -- tests-zkevm@v0.5.0 (execution-specs) support (~12,000 fixtures) - -### Out of Scope - -- Consensus layer (beacon chain) changes — ethrex is an EL client -- P2P gossip topics, req/resp protocols, MetaData/ENR changes (CL concerns) -- BLS signature verification of `SignedExecutionProof` (CL concern) -- Prover whitelist management (CL concern, uses validator set) - ---- - -## Implementation Status - -### Completed - -- **Phase 1**: Shared prover infrastructure (`crates/prover/`) — backends, protocol, GenServer pull loop -- **Phase 2**: SSZ types and EIP-8025 Engine API types -- **Phase 3**: L1 ProofCoordinator — coordinator, config, proof storage, callback delivery -- **Phase 4**: Engine API endpoints — all three handlers wired into the RPC router -- **Phase 5**: Guest program — EIP-8025 input/output format, payload-to-block conversion (T5.1–T5.3) -- **Phase 7**: zkevm test infrastructure (T7.1–T7.3) — fixture parsing, download targets, stateless test path - -### Remaining - -- **T5.4**: Update zkVM guest binaries (needs real zkVM testing) -- **T6.1–T6.4**: Feature flag wiring, CLI integration, node initialization -- **T7.4–T7.8**: Output comparison, SSZ unit tests, integration tests, L2 regression tests +**In scope.** The stateless-validator guest and its wire contract; the shared +validation path used by both the EXECUTE precompile and the zkVM guests; per-zkVM +crypto providers; the release pipeline for ELFs and verification keys; generated +conformance vectors. + +**Out of scope.** Consensus-layer changes; P2P gossip and req/resp; BLS +verification of `SignedExecutionProof`; prover whitelist management. All are CL +concerns. + +**Known follow-ups.** + +- The `libssz-merkle` progressive child-order fix, without which every published + root is wrong. +- The 6 vectors that differ beyond the root. +- `eth-act/ere-guests` moving to the #3248+#3278 dialect; until it does, our + artifacts are not drop-in for it. +- Populating `public_keys` in the native-rollup producer so the EXECUTE path can + enforce the check the guest already performs (`TODO(#6716)`). +- Minisign signing of release assets, as ere-guests does. From cbc72eb7dbc84ad84a5ae10d79da2e0826f2ddb5 Mon Sep 17 00:00:00 2001 From: Lucas Fiegl Date: Wed, 5 Aug 2026 11:12:38 -0300 Subject: [PATCH 17/30] feat(l2): populate stateless public_keys --- crates/blockchain/stateless.rs | 37 +-- crates/common/crypto/provider.rs | 36 +++ crates/common/types/transaction.rs | 48 +++- crates/guest-program/src/l1/program.rs | 60 ++--- .../l2/sequencer/native_rollup/l1_advancer.rs | 42 +++- crates/vm/levm/src/execute_precompile.rs | 221 +++++++++++++----- docs/eip-8025.md | 2 - docs/vm/levm/native_rollups.md | 31 ++- test/tests/l2/ssz_round_trip.rs | 117 +++++++++- 9 files changed, 463 insertions(+), 131 deletions(-) diff --git a/crates/blockchain/stateless.rs b/crates/blockchain/stateless.rs index 688d0c467b5..c0ab951eb52 100644 --- a/crates/blockchain/stateless.rs +++ b/crates/blockchain/stateless.rs @@ -10,9 +10,7 @@ use std::sync::Arc; use ethrex_common::types::block_execution_witness::ExecutionWitness; -use ethrex_common::types::stateless_ssz::{ - NewPayloadRequest, SszStatelessInput, SszStatelessValidationResult, -}; +use ethrex_common::types::stateless_ssz::{SszStatelessInput, SszStatelessValidationResult}; use ethrex_crypto::Crypto; use ethrex_guest_program::common::ExecutionError; use ethrex_guest_program::l1::verify_stateless_block; @@ -21,20 +19,23 @@ use libssz_merkle::{HashTreeRoot, Sha2Hasher}; /// Core stateless validation function matching the execution-specs definition. /// -/// Takes a `NewPayloadRequest`, `ExecutionWitness`, and `ChainConfig`, and: +/// Takes the decoded `StatelessInput` plus the `ExecutionWitness` rebuilt from +/// it, and: /// 1. Computes `hash_tree_root` of the `NewPayloadRequest` -/// 2. Converts the payload to a `Block` +/// 2. Converts the payload to a `Block` and checks the supplied public keys /// 3. Executes the block statelessly /// 4. Returns the validation result +/// +/// The witness is rebuilt by the caller rather than here so that a malformed +/// witness maps to a precompile-level failure — see [`StatelessExecutor::verify`]. pub fn verify_stateless_new_payload( - new_payload_request: &NewPayloadRequest, + input: &SszStatelessInput, execution_witness: ExecutionWitness, - chain_id: u64, crypto: Arc, ) -> SszStatelessValidationResult { - let request_root = new_payload_request.hash_tree_root(&Sha2Hasher); + let request_root = input.new_payload_request.hash_tree_root(&Sha2Hasher); - let successful = match verify_inner(new_payload_request, execution_witness, crypto) { + let successful = match verify_inner(input, execution_witness, crypto) { Ok(()) => true, Err(e) => { tracing::error!("stateless validation failed: {e}"); @@ -47,17 +48,22 @@ pub fn verify_stateless_new_payload( SszStatelessValidationResult { new_payload_request_root: request_root, successful_validation: successful, - chain_id, + chain_id: input.chain_id, schema_id: ethrex_common::types::stateless_ssz::STATELESS_INPUT_SCHEMA_ID, } } fn verify_inner( - new_payload_request: &NewPayloadRequest, + input: &SszStatelessInput, execution_witness: ExecutionWitness, crypto: Arc, ) -> Result<(), ExecutionError> { - verify_stateless_block(new_payload_request, execution_witness, crypto) + verify_stateless_block( + &input.new_payload_request, + &input.public_keys, + execution_witness, + crypto, + ) } /// Concrete `StatelessValidator` used by the EXECUTE precompile: deserializes @@ -91,12 +97,7 @@ impl ethrex_vm::StatelessValidator for StatelessExecutor { let execution_witness = ExecutionWitness::from_ssz(input) .map_err(|_| VMError::from(PrecompileError::ExecuteInvalidInput))?; - let result = verify_stateless_new_payload( - &input.new_payload_request, - execution_witness, - input.chain_id, - self.crypto.clone(), - ); + let result = verify_stateless_new_payload(input, execution_witness, self.crypto.clone()); let mut buf = Vec::new(); result.ssz_append(&mut buf); diff --git a/crates/common/crypto/provider.rs b/crates/common/crypto/provider.rs index 5eca02782db..7975918c258 100644 --- a/crates/common/crypto/provider.rs +++ b/crates/common/crypto/provider.rs @@ -162,6 +162,42 @@ pub trait Crypto: Send + Sync + core::fmt::Debug { /// Recover the signer address from a 65-byte signature (r||s||v) + 32-byte message hash. /// Used by transaction validation (tx.sender()) and EIP-7702 authority recovery. + /// Recover the signer's **uncompressed** secp256k1 public key (`0x04 || X || Y`). + /// + /// [`Self::recover_signer`] hashes this and keeps the last 20 bytes; the + /// stateless-validation wire format needs the key itself, so that a guest can + /// verify signatures against a supplied key instead of running `ecrecover`. + /// Applies the same EIP-2 low-s rejection, so the two agree on which + /// signatures are valid. + /// + /// Host-only: this exists to *produce* `SszStatelessInput::public_keys`. Guests + /// only ever consume them, so no zkVM provider needs to override it. + #[cfg(feature = "secp256k1")] + fn recover_public_key(&self, sig: &[u8; 65], msg: &[u8; 32]) -> Result<[u8; 65], CryptoError> { + // EIP-2: reject high-s signatures (s > secp256k1n/2), matching recover_signer. + const SECP256K1_N_HALF: [u8; 32] = + hex_literal::hex!("7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0"); + if sig[32..64] > SECP256K1_N_HALF[..] { + return Err(CryptoError::InvalidSignature); + } + + let recovery_id = secp256k1::ecdsa::RecoveryId::try_from(sig[64] as i32) + .map_err(|_| CryptoError::InvalidRecoveryId)?; + let recoverable_sig = secp256k1::ecdsa::RecoverableSignature::from_compact( + sig[..64] + .try_into() + .map_err(|_| CryptoError::InvalidSignature)?, + recovery_id, + ) + .map_err(|_| CryptoError::InvalidSignature)?; + + let public_key = recoverable_sig + .recover(&secp256k1::Message::from_digest(*msg)) + .map_err(|_| CryptoError::RecoveryFailed)?; + + Ok(public_key.serialize_uncompressed()) + } + fn recover_signer(&self, sig: &[u8; 65], msg: &[u8; 32]) -> Result { // EIP-2: reject high-s signatures (s > secp256k1n/2) const SECP256K1_N_HALF: [u8; 32] = diff --git a/crates/common/types/transaction.rs b/crates/common/types/transaction.rs index d2f9f73a06d..5c66f92e8fb 100644 --- a/crates/common/types/transaction.rs +++ b/crates/common/types/transaction.rs @@ -1208,6 +1208,10 @@ impl RLPDecode for FeeTokenTransaction { } } +/// The bytes a transaction's signature covers, paired with the 65-byte +/// `r || s || v` signature itself. See [`Transaction::signing_payload`]. +pub type SigningPayload = (Vec, [u8; 65]); + impl Transaction { pub fn sender(&self, crypto: &dyn Crypto) -> Result { // Frame transactions have explicit sender, no ECDSA recovery @@ -1247,7 +1251,15 @@ impl Transaction { .copied() } - fn compute_sender(&self, crypto: &dyn Crypto) -> Result { + /// The bytes that were signed, plus the 65-byte `r || s || v` signature. + /// + /// `Ok(None)` for transactions that carry an explicit sender and no signature + /// (privileged L2 and frame), for which there is nothing to recover. + /// + /// Exposed so callers needing the *public key* rather than the address can + /// reuse this logic: the per-variant EIP-155 and typed-transaction handling + /// below is subtle and must exist in exactly one place. + pub fn signing_payload(&self) -> Result, CryptoError> { let (buf, sig) = match self { Transaction::LegacyTransaction(tx) => { let v = u64::try_from(tx.v).map_err(|_| CryptoError::InvalidSignature)?; @@ -1373,8 +1385,10 @@ impl Transaction { sig[64] = tx.signature_y_parity as u8; (buf, sig) } - Transaction::PrivilegedL2Transaction(tx) => return Ok(tx.from), - Transaction::FrameTransaction(tx) => return Ok(tx.sender), + // Explicit sender, no signature: nothing to recover from. + Transaction::PrivilegedL2Transaction(_) | Transaction::FrameTransaction(_) => { + return Ok(None); + } Transaction::FeeTokenTransaction(tx) => { let mut buf = vec![self.tx_type() as u8]; Encoder::new(&mut buf) @@ -1396,10 +1410,38 @@ impl Transaction { (buf, sig) } }; + Ok(Some((buf, sig))) + } + + fn compute_sender(&self, crypto: &dyn Crypto) -> Result { + match self { + Transaction::PrivilegedL2Transaction(tx) => return Ok(tx.from), + Transaction::FrameTransaction(tx) => return Ok(tx.sender), + _ => {} + } + let Some((buf, sig)) = self.signing_payload()? else { + // Unreachable: the two signature-less variants are handled above. + return Err(CryptoError::InvalidSignature); + }; let msg = crypto.keccak256(&buf); crypto.recover_signer(&sig, &msg) } + /// The signer's uncompressed secp256k1 public key (`0x04 || X || Y`). + /// + /// `Ok(None)` for privileged L2 and frame transactions, which carry an explicit + /// sender and no signature. Used to populate + /// `SszStatelessInput::public_keys`, which the stateless guest checks against + /// each transaction's recovered sender. + #[cfg(feature = "secp256k1")] + pub fn public_key(&self, crypto: &dyn Crypto) -> Result, CryptoError> { + let Some((buf, sig)) = self.signing_payload()? else { + return Ok(None); + }; + let msg = crypto.keccak256(&buf); + crypto.recover_public_key(&sig, &msg).map(Some) + } + pub fn gas_limit(&self) -> u64 { match self { Transaction::LegacyTransaction(tx) => tx.gas, diff --git a/crates/guest-program/src/l1/program.rs b/crates/guest-program/src/l1/program.rs index 3fdf93b0a3d..24052c9da1a 100644 --- a/crates/guest-program/src/l1/program.rs +++ b/crates/guest-program/src/l1/program.rs @@ -172,9 +172,6 @@ pub fn new_payload_request_to_block( Ok(Block::new(header, body)) } -/// Core stateless block validation for the native-rollup EXECUTE path. -/// -/// Sole caller: `ethrex-blockchain`'s `verify_stateless_new_payload` /// Validate the per-transaction public keys carried by a stateless input. /// /// The spec's `StatelessInput` supplies one uncompressed secp256k1 key per @@ -182,19 +179,10 @@ pub fn new_payload_request_to_block( /// the recovered sender must reject the payload (issue #6716). /// /// Hoisted out of the now-deleted duplicate validation family, which was the -/// only place this check existed. Two consequences worth being explicit about: -/// -/// 1. The **guest** path must call this, or deduplicating the two validation -/// families silently drops a spec-required check. -/// 2. The **EXECUTE precompile** path ([`verify_stateless_block`]) deliberately -/// does *not* call it, and never has. `build_ssz_stateless_input` in the L2 -/// advancer sends `public_keys` empty ("Empty for now") while L2 blocks do -/// carry transactions, so enforcing the length check there would reject every -/// native-rollup block. Enabling it requires the producer to populate real -/// keys first. -/// -/// TODO(#6716): populate `public_keys` in the native-rollup producer and call -/// this from `verify_stateless_block`, so both paths enforce the spec. +/// only place this check existed. It is called from [`verify_stateless_block`], +/// which is the single point both the zkVM guest and the EXECUTE precompile pass +/// through, so neither path can drop it (#6716). That placement is deliberate: +/// while the check lived in a caller, one of the two callers did not have it. /// /// Upstream `build_stateless_input` *skips* keys for undecodable or /// bad-signature transactions, so `public_keys.len()` can legitimately be @@ -243,21 +231,20 @@ pub fn validate_public_keys( Ok(()) } -/// (`StatelessExecutor`, the `StatelessValidator` trait impl invoked by the -/// EXECUTE precompile). NOTE: the zkVM guest binaries do **not** call this — -/// the zkVM guest binaries now route here too, via [`run_stateless_guest`] — -/// there is no longer a separate duplicate validation family. +/// Stateless validation of a single payload, shared by every entrypoint. /// /// Implements the `verify_stateless_new_payload` logic from execution-specs: -/// reconstruct block → validate versioned hashes → execute statelessly → -/// inject recomputed `burned_fees` → validate the recomputed block access list -/// hash (Amsterdam+) → verify `block_hash`. +/// reconstruct block → check the supplied public keys → validate versioned +/// hashes → execute statelessly → inject recomputed `burned_fees` → validate the +/// recomputed block access list hash (Amsterdam+) → verify `block_hash`. /// -/// Always compiled: `verify_inner` in `ethrex-blockchain` calls this on the -/// EXECUTE precompile path, and the zkVM guest reaches it through -/// [`run_stateless_guest`]. +/// Always compiled, and reached by both entrypoints: the zkVM guests via +/// [`run_stateless_guest`], and the EXECUTE precompile via `verify_inner` in +/// `ethrex-blockchain`. Everything a payload must satisfy belongs here rather +/// than in a caller — see [`validate_public_keys`] for what splitting it cost. pub fn verify_stateless_block( new_payload_request: ðrex_common::types::stateless_ssz::NewPayloadRequest, + public_keys: &SszPublicKeys, execution_witness: ethrex_common::types::block_execution_witness::ExecutionWitness, crypto: Arc, ) -> Result<(), ExecutionError> { @@ -270,6 +257,10 @@ pub fn verify_stateless_block( let block = new_payload_request_to_block(new_payload_request, crypto.as_ref()) .map_err(|e| ExecutionError::Internal(format!("payload conversion: {e}")))?; + // Check the supplied keys against the recovered senders before committing to + // execution, so a mismatched key rejects without paying for a block. + validate_public_keys(public_keys, &block, crypto.as_ref())?; + // Keep block in a fixed-size array so we can reclaim it after execute_blocks // (which borrows it as &[Block] without consuming it). let blocks = [block]; @@ -365,8 +356,8 @@ pub fn run_stateless_guest(input_bytes: &[u8], crypto: Arc) -> Vec, @@ -375,13 +366,12 @@ pub fn validate_stateless_execution( ethrex_common::types::block_execution_witness::ExecutionWitness::from_ssz(input) .map_err(|e| ExecutionError::Internal(format!("witness rebuild: {e}")))?; - // Reconstruct the block once so the public keys can be checked against its - // recovered senders before committing to execution. - let block = new_payload_request_to_block(&input.new_payload_request, crypto.as_ref()) - .map_err(|e| ExecutionError::Internal(format!("payload conversion: {e}")))?; - validate_public_keys(&input.public_keys, &block, crypto.as_ref())?; - - verify_stateless_block(&input.new_payload_request, execution_witness, crypto) + verify_stateless_block( + &input.new_payload_request, + &input.public_keys, + execution_witness, + crypto, + ) } /// Validate blocks statelessly against an in-memory witness. diff --git a/crates/l2/sequencer/native_rollup/l1_advancer.rs b/crates/l2/sequencer/native_rollup/l1_advancer.rs index 4aaa9fb3d23..f09c2a248ee 100644 --- a/crates/l2/sequencer/native_rollup/l1_advancer.rs +++ b/crates/l2/sequencer/native_rollup/l1_advancer.rs @@ -277,7 +277,7 @@ pub fn build_ssz_stateless_input( ) -> Result, String> { use ethrex_common::types::stateless_ssz::*; use libssz::SszEncode; - use libssz_types::{ProgressiveList, SszList}; + use libssz_types::{ProgressiveList, SszList, SszVector}; // 1. Convert Block → SSZ ExecutionPayload // Both levels are progressive lists since #3248, so neither can overflow a @@ -289,6 +289,36 @@ pub fn build_ssz_stateless_input( .collect(); let ssz_transactions: ProgressiveList> = transactions.into(); + // One uncompressed secp256k1 key per transaction, in transaction order, so the + // consumer can check senders without running `ecrecover` (#6716). Derived from + // the same `body.transactions` above, so the length and ordering the consumer + // requires hold by construction. + // + // Every transaction in a native-rollup block is signature-bearing: L1→L2 + // messages are relayed as signed EIP-1559 transactions, and the EXECUTE + // precompile rejects the signature-less variants (privileged and frame) + // outright. `public_key` therefore returns `Some` for all of them, and a `None` + // is a real inconsistency rather than a case to skip — skipping would shorten + // the list and be rejected as a length mismatch anyway, with a far less + // informative message. + let public_keys = body + .transactions + .iter() + .enumerate() + .map(|(i, tx)| { + let key = tx + .public_key(&NativeCrypto) + .map_err(|e| format!("failed to recover public key for transaction {i}: {e}"))? + .ok_or_else(|| { + format!("transaction {i} carries no signature to recover a public key from") + })?; + SszVector::try_from(key.to_vec()) + .map_err(|e| format!("public key for transaction {i} is not 65 bytes: {e:?}")) + }) + .collect::, String>>()?; + let ssz_public_keys: SszPublicKeys = SszList::try_from(public_keys) + .map_err(|e| format!("public_keys exceeds MAX_PUBLIC_KEYS: {e:?}"))?; + let ssz_withdrawals = ProgressiveList::new(); // Empty for L2 // base_fee_per_gas as LE uint256 @@ -366,7 +396,7 @@ pub fn build_ssz_stateless_input( // #3278: only the chain id crosses the wire. The consumer derives the rest // from (chain_id, fork), with the fork coming from the schema-id prefix. chain_id: witness.chain_config.chain_id, - public_keys: SszList::new(), // Empty for now + public_keys: ssz_public_keys, }; // 5. Serialize to schema-prefixed SSZ bytes. @@ -627,9 +657,7 @@ mod tests { .expect("SSZ encoding should succeed"); // SSZ → deserialize → reconstruct Block - use ethrex_common::types::stateless_ssz::SszStatelessInput; - use libssz::SszDecode; - let input = SszStatelessInput::from_ssz_bytes(&ssz_bytes) + let input = ethrex_guest_program::l1::decode_stateless_input(&ssz_bytes) .expect("SSZ deserialization should succeed"); let reconstructed_block = ethrex_guest_program::l1::new_payload_request_to_block( @@ -811,9 +839,7 @@ mod tests { .expect("SSZ encoding should succeed for Amsterdam block with BAL"); // SSZ → deserialize → reconstruct Block - use ethrex_common::types::stateless_ssz::SszStatelessInput; - use libssz::SszDecode; - let input = SszStatelessInput::from_ssz_bytes(&ssz_bytes) + let input = ethrex_guest_program::l1::decode_stateless_input(&ssz_bytes) .expect("SSZ deserialization should succeed"); let reconstructed_block = ethrex_guest_program::l1::new_payload_request_to_block( diff --git a/crates/vm/levm/src/execute_precompile.rs b/crates/vm/levm/src/execute_precompile.rs index e78d6e6cc57..899c8623798 100644 --- a/crates/vm/levm/src/execute_precompile.rs +++ b/crates/vm/levm/src/execute_precompile.rs @@ -140,12 +140,28 @@ fn validate_l2_constraints( return Err(PrecompileError::ExecuteInvalidInput.into()); } let reqs = &input.new_payload_request.execution_requests; - if !reqs.deposits.is_empty() || !reqs.withdrawals.is_empty() || !reqs.consolidations.is_empty() + if !reqs.deposits.is_empty() + || !reqs.withdrawals.is_empty() + || !reqs.consolidations.is_empty() + || !reqs.builder_deposits.is_empty() + || !reqs.builder_exits.is_empty() { return Err(PrecompileError::ExecuteInvalidInput.into()); } for tx_bytes in payload.transactions.iter() { - if let Some(&0x03) = tx_bytes.iter().next() { + // Rejected transaction types, by leading EIP-2718 envelope byte: + // + // - `0x03` (EIP-4844): L2 blocks carry no blobs, and `blob_gas_used` / + // `excess_blob_gas` are already pinned to zero above. + // - `0x06` (frame) and `0x7e` (privileged): these carry an explicit sender + // and no signature, so no public key can be recovered for them. The + // stateless input commits one key per transaction, so admitting them + // would make a well-formed block unrepresentable. Native rollups do not + // need them: L1→L2 messages are relayed as signed EIP-1559 transactions + // (see `block_producer.rs`), so rejecting them here costs no + // functionality and turns "every transaction is signature-bearing" from + // an incidental property of the current producer into an enforced one. + if let Some(&(0x03 | 0x06 | 0x7e)) = tx_bytes.iter().next() { return Err(PrecompileError::ExecuteInvalidInput.into()); } } @@ -164,8 +180,9 @@ mod tests { use super::{EXECUTE_GAS_PER_WITNESS_BYTE, run_execute}; use bytes::Bytes; use ethrex_common::types::stateless_ssz::{ - Bytes20, DepositRequest, ExecutionPayload, ExecutionRequests, NewPayloadRequest, - SszExecutionWitness, SszStatelessInput, SszStatelessValidationResult, Withdrawal, + BuilderDepositRequest, BuilderExitRequest, Bytes20, DepositRequest, ExecutionPayload, + ExecutionRequests, NewPayloadRequest, SszExecutionWitness, SszStatelessInput, + SszStatelessValidationResult, Withdrawal, }; use libssz::SszEncode; @@ -190,6 +207,35 @@ mod tests { } } + /// Encode an input as `statelessInputBytes`: the 2-byte big-endian schema id + /// followed by the SSZ body. + /// + /// Every test must go through this. Building the body alone makes `run_execute` + /// reject at the prefix check, which silently turns each constraint test below + /// into a test of the prefix check — they pass without ever evaluating the + /// constraint they name. + fn calldata_for(input: &SszStatelessInput) -> Bytes { + let mut buf = ethrex_common::types::stateless_ssz::STATELESS_INPUT_SCHEMA_ID + .to_be_bytes() + .to_vec(); + input.ssz_append(&mut buf); + Bytes::from(buf) + } + + /// Assert an error is the constraint rejection `ExecuteInvalidInput`, and is a + /// CALL-level failure (includable, attacker pays) rather than a tx-abort. + fn assert_invalid_input(err: &crate::errors::VMError, what: &str) { + assert_eq!( + err, + &crate::errors::VMError::from(crate::errors::PrecompileError::ExecuteInvalidInput), + "{what} must be rejected as ExecuteInvalidInput; got: {err:?}" + ); + assert!( + !err.should_propagate(), + "{what} must be a CALL-level failure (non-propagating); got: {err:?}" + ); + } + /// Build a minimal L2-valid `SszStatelessInput` with the given gas fields. /// No blobs, no withdrawals, no execution requests — satisfies all /// `validate_l2_constraints` checks. @@ -256,9 +302,7 @@ mod tests { #[test] fn execute_fails_closed_on_invalid() { let input = l2_valid_input(0, 1_000_000); - let mut calldata_buf = Vec::new(); - input.ssz_append(&mut calldata_buf); - let calldata = Bytes::from(calldata_buf); + let calldata = calldata_for(&input); // Invalid result → must Err (fail-closed) with a NON-propagating error so the tx is // INCLUDABLE and the attacker pays Task 1's gas charge (I1×I13 regression guard). @@ -284,9 +328,7 @@ mod tests { #[test] fn execute_charges_gas_limit_not_gas_used() { let input = l2_valid_input(0, 1_000_000); - let mut calldata_buf = Vec::new(); - input.ssz_append(&mut calldata_buf); - let calldata = Bytes::from(calldata_buf); + let calldata = calldata_for(&input); let start_gas = 100_000_000u64; let mut gas_remaining = start_gas; @@ -325,10 +367,7 @@ mod tests { let mut gas = 100_000_000u64; let err = run_execute(&MockValidator, &calldata, &mut gas) .expect_err("malformed SSZ input must be rejected"); - assert!( - !err.should_propagate(), - "malformed EXECUTE input must be a CALL-level failure (non-propagating), not a tx-abort; got: {err:?}" - ); + assert_invalid_input(&err, "malformed SSZ input"); } // ── Negative constraint tests (I11) ────────────────────────────────────── @@ -346,16 +385,11 @@ mod tests { fn execute_rejects_blob_gas_used_nonzero() { let mut input = l2_valid_input(0, 1_000_000); input.new_payload_request.execution_payload.blob_gas_used = 1; - let mut buf = Vec::new(); - input.ssz_append(&mut buf); - let calldata = Bytes::from(buf); + let calldata = calldata_for(&input); let mut gas = 100_000_000u64; let err = run_execute(&MockValidator, &calldata, &mut gas) .expect_err("blob_gas_used != 0 must be rejected"); - assert!( - !err.should_propagate(), - "blob_gas_used violation must be a CALL-level failure (non-propagating); got: {err:?}" - ); + assert_invalid_input(&err, "blob_gas_used != 0"); } /// Constraint 2: `excess_blob_gas` must be zero. @@ -363,16 +397,11 @@ mod tests { fn execute_rejects_excess_blob_gas_nonzero() { let mut input = l2_valid_input(0, 1_000_000); input.new_payload_request.execution_payload.excess_blob_gas = 1; - let mut buf = Vec::new(); - input.ssz_append(&mut buf); - let calldata = Bytes::from(buf); + let calldata = calldata_for(&input); let mut gas = 100_000_000u64; let err = run_execute(&MockValidator, &calldata, &mut gas) .expect_err("excess_blob_gas != 0 must be rejected"); - assert!( - !err.should_propagate(), - "excess_blob_gas violation must be a CALL-level failure (non-propagating); got: {err:?}" - ); + assert_invalid_input(&err, "excess_blob_gas != 0"); } /// Constraint 3: `withdrawals` list must be empty. @@ -387,16 +416,11 @@ mod tests { }] .try_into() .expect("withdrawals"); - let mut buf = Vec::new(); - input.ssz_append(&mut buf); - let calldata = Bytes::from(buf); + let calldata = calldata_for(&input); let mut gas = 100_000_000u64; let err = run_execute(&MockValidator, &calldata, &mut gas) .expect_err("non-empty withdrawals must be rejected"); - assert!( - !err.should_propagate(), - "withdrawals violation must be a CALL-level failure (non-propagating); got: {err:?}" - ); + assert_invalid_input(&err, "non-empty withdrawals"); } /// Constraint 4: `execution_requests` (deposits/withdrawals/consolidations) must all be empty. @@ -413,35 +437,120 @@ mod tests { }] .try_into() .expect("deposits"); - let mut buf = Vec::new(); - input.ssz_append(&mut buf); - let calldata = Bytes::from(buf); + let calldata = calldata_for(&input); let mut gas = 100_000_000u64; let err = run_execute(&MockValidator, &calldata, &mut gas) .expect_err("non-empty execution_requests must be rejected"); - assert!( - !err.should_propagate(), - "execution_requests violation must be a CALL-level failure (non-propagating); got: {err:?}" - ); + assert_invalid_input(&err, "non-empty execution_requests"); } - /// Constraint 5: no transaction may have type byte `0x03` (blob tx). + /// Constraint 4b: the EIP-8282 builder request lists must be empty too. They + /// were added to `ExecutionRequests` after the original three, so this pins + /// them against being reached by the constraint check only by accident. #[test] - fn execute_rejects_blob_typed_transaction() { + fn execute_rejects_nonempty_builder_requests() { let mut input = l2_valid_input(0, 1_000_000); - // A minimal blob-typed transaction: first byte is 0x03. - let blob_tx = vec![0x03u8, 0x00, 0x00].try_into().expect("blob_tx bytes"); + input + .new_payload_request + .execution_requests + .builder_deposits = vec![BuilderDepositRequest { + pubkey: [0u8; 48], + withdrawal_credentials: [0u8; 32], + amount: 1, + signature: [0u8; 96], + }] + .try_into() + .expect("builder_deposits"); + let mut gas = 100_000_000u64; + let err = run_execute(&MockValidator, &calldata_for(&input), &mut gas) + .expect_err("non-empty builder_deposits must be rejected"); + assert_invalid_input(&err, "non-empty builder_deposits"); + + let mut input = l2_valid_input(0, 1_000_000); + input.new_payload_request.execution_requests.builder_exits = vec![BuilderExitRequest { + source_address: Bytes20([0u8; 20]), + pubkey: [0u8; 48], + }] + .try_into() + .expect("builder_exits"); + let mut gas = 100_000_000u64; + let err = run_execute(&MockValidator, &calldata_for(&input), &mut gas) + .expect_err("non-empty builder_exits must be rejected"); + assert_invalid_input(&err, "non-empty builder_exits"); + } + + /// Constraint 5: no transaction may carry a rejected EIP-2718 type byte. + /// + /// `0x03` is a blob transaction. `0x06` (frame) and `0x7e` (privileged) carry + /// an explicit sender and no signature, so no public key can be recovered for + /// them — and the stateless input commits one key per transaction. Rejecting + /// them here is what makes "every transaction in the payload is + /// signature-bearing" an enforced invariant rather than a property of the + /// current producer, which `build_ssz_stateless_input` relies on to populate + /// `public_keys` without gaps. + #[test] + fn execute_rejects_unsupported_transaction_types() { + for (type_byte, what) in [ + (0x03u8, "blob-typed transaction"), + (0x06u8, "frame transaction"), + (0x7eu8, "privileged transaction"), + ] { + let mut input = l2_valid_input(0, 1_000_000); + let tx = vec![type_byte, 0x00, 0x00].try_into().expect("tx bytes"); + input.new_payload_request.execution_payload.transactions = + vec![tx].try_into().expect("transactions"); + let calldata = calldata_for(&input); + let mut gas = 100_000_000u64; + let err = run_execute(&MockValidator, &calldata, &mut gas) + .expect_err("unsupported transaction type must be rejected"); + assert_invalid_input(&err, what); + } + + // A type byte just outside the rejected set must still be accepted, so the + // test cannot pass by rejecting everything. + let mut input = l2_valid_input(0, 1_000_000); + let tx = vec![0x02u8, 0x00, 0x00].try_into().expect("tx bytes"); input.new_payload_request.execution_payload.transactions = - vec![blob_tx].try_into().expect("transactions"); - let mut buf = Vec::new(); - input.ssz_append(&mut buf); - let calldata = Bytes::from(buf); + vec![tx].try_into().expect("transactions"); + let calldata = calldata_for(&input); let mut gas = 100_000_000u64; - let err = run_execute(&MockValidator, &calldata, &mut gas) - .expect_err("blob-typed transaction must be rejected"); - assert!( - !err.should_propagate(), - "blob tx type violation must be a CALL-level failure (non-propagating); got: {err:?}" - ); + run_execute(&MockValidator, &calldata, &mut gas) + .expect("an EIP-1559 transaction must be accepted"); + } + + /// The schema prefix is mandatory, and only `STATELESS_INPUT_SCHEMA_ID` is + /// accepted. Since #3278 removed chain configuration from the wire, the prefix + /// is the sole carrier of the fork, so an unexpected id must reject rather than + /// be validated under Amsterdam rules by default. + #[test] + fn execute_requires_the_expected_schema_prefix() { + let input = l2_valid_input(0, 1_000_000); + let mut body = Vec::new(); + input.ssz_append(&mut body); + + // No prefix at all: the body's first bytes are an SSZ offset, not a schema id. + let mut gas = 100_000_000u64; + let err = run_execute(&MockValidator, &Bytes::from(body.clone()), &mut gas) + .expect_err("unprefixed input must be rejected"); + assert_invalid_input(&err, "unprefixed input"); + + // A well-formed but different schema id. + let mut wrong = vec![0x15u8, 0x02]; + wrong.extend_from_slice(&body); + let mut gas = 100_000_000u64; + let err = run_execute(&MockValidator, &Bytes::from(wrong), &mut gas) + .expect_err("unexpected schema id must be rejected"); + assert_invalid_input(&err, "unexpected schema id"); + + // Too short to hold an id at all. + let mut gas = 100_000_000u64; + let err = run_execute(&MockValidator, &Bytes::from(vec![0x15u8]), &mut gas) + .expect_err("truncated input must be rejected"); + assert_invalid_input(&err, "input too short for a schema id"); + + // And the expected prefix is accepted, so the test is not vacuous. + let mut gas = 100_000_000u64; + run_execute(&MockValidator, &calldata_for(&input), &mut gas) + .expect("the expected schema prefix must be accepted"); } } diff --git a/docs/eip-8025.md b/docs/eip-8025.md index 6ce3c1194b9..a55ebe76b91 100644 --- a/docs/eip-8025.md +++ b/docs/eip-8025.md @@ -266,6 +266,4 @@ concerns. - The 6 vectors that differ beyond the root. - `eth-act/ere-guests` moving to the #3248+#3278 dialect; until it does, our artifacts are not drop-in for it. -- Populating `public_keys` in the native-rollup producer so the EXECUTE path can - enforce the check the guest already performs (`TODO(#6716)`). - Minisign signing of release assets, as ere-guests does. diff --git a/docs/vm/levm/native_rollups.md b/docs/vm/levm/native_rollups.md index 54398c9def6..d019d4f2ed1 100644 --- a/docs/vm/levm/native_rollups.md +++ b/docs/vm/levm/native_rollups.md @@ -49,8 +49,8 @@ SSZ-encoded `StatelessInput`: pub struct SszStatelessInput { pub new_payload_request: NewPayloadRequest, // Full block as SSZ pub witness: SszExecutionWitness, // State trie, storage tries, codes - pub chain_config: SszChainConfig, // chain_id - pub public_keys: SszList<...>, // Pre-recovered tx public keys (stub) + pub chain_id: u64, // #3278: the only config on the wire + pub public_keys: SszList<...>, // One recovered tx public key per tx } ``` @@ -81,8 +81,19 @@ Before delegating to `verify_stateless_new_payload`, the precompile validates: - `blob_gas_used == 0` — no blob data on L2 - `excess_blob_gas == 0` — no blob fee market on L2 - `withdrawals` is empty — L2 doesn't have consensus-layer withdrawals -- `execution_requests` is empty — no deposit/withdrawal/consolidation requests -- No type-3 (blob) transactions in the transaction list +- `execution_requests` is empty — no deposit, withdrawal, consolidation, builder-deposit or builder-exit requests +- No type-`0x03` (blob), `0x06` (frame) or `0x7e` (privileged) transactions in the transaction list + +The two signature-less types are rejected for a structural reason, not a policy +one: they carry an explicit sender and no signature, so no public key can be +recovered for them, while `StatelessInput` commits exactly one key per +transaction. Native rollups do not need them — L1→L2 messages are relayed as +signed EIP-1559 transactions — so rejecting them makes "every transaction in the +payload is signature-bearing" an invariant the producer can rely on. + +The 2-byte schema-id prefix is also required, and only `0x1501` is accepted: +since execution-specs #3278 no chain configuration crosses the wire, so the +prefix is the sole carrier of the fork. ### Gas Charging @@ -162,8 +173,13 @@ The `GuestProgramStateDb` adapter (`crates/vm/levm/src/db/guest_program_state_db 2. Validates block headers from the witness 3. Builds `GuestProgramState` from the witness 4. Converts the SSZ `NewPayloadRequest` → ethrex `Block` -5. Executes the block via LEVM -6. Returns `StatelessValidationResult` with the hash tree root and a `successful_validation` flag +5. Checks each supplied public key derives to that transaction's recovered sender +6. Executes the block via LEVM +7. Returns `StatelessValidationResult` with the hash tree root, `chain_id`, `schema_id` and a `successful_validation` flag + +Steps 4–7 live in `verify_stateless_block` (`crates/guest-program/src/l1/program.rs`), +which is the single point both this path and the zkVM guests pass through — so +neither can enforce a different set of checks than the other. The state root check implicitly guarantees both correct L1 message processing and correct L2→L1 withdrawal recording. @@ -277,7 +293,7 @@ Without `eip-8025`: | ZK variant | Specified (proof-carrying tx + PROOFROOT) | Not implemented (re-execution only) | **Gap (by design)** | | Forced transactions | WIP (FOCIL) | Not implemented | **Gap** | | DA cost pricing | WIP | Not implemented | **Both WIP** | -| `public_keys` | Pre-recovered tx keys | Empty tuple (stub) | **Stub** | +| `public_keys` | Pre-recovered tx keys | Populated by the advancer, checked against recovered senders | **Aligned** | ### EIP-8079 divergences @@ -296,4 +312,3 @@ This PoC intentionally omits several things that would be needed for production: - **No L1 message inclusion deadline** — `pendingL1Messages` have no per-message deadline, so the advancer can defer processing them indefinitely without on-chain consequence. `OnChainProposer.sol` enforces this for privileged transactions via `PRIVILEGED_TX_MAX_WAIT_BEFORE_INCLUSION` + `hasExpiredPrivilegedTransactions()`; the analogous mechanism for `NativeRollup.sol` is a TODO. - **L2 ETH supply drain** — Base fees are burned but not credited back on L2. A production solution would use a `BaseFeeVault` pattern. - **No blob data support** — Only calldata-based input (spec proposes blob references via EIP-8142) -- **`public_keys` empty** — Pre-recovered transaction public keys are not populated yet diff --git a/test/tests/l2/ssz_round_trip.rs b/test/tests/l2/ssz_round_trip.rs index 552b68c3f94..1df103d5f0f 100644 --- a/test/tests/l2/ssz_round_trip.rs +++ b/test/tests/l2/ssz_round_trip.rs @@ -12,8 +12,9 @@ use ethrex_common::types::stateless_ssz::{ }; use ethrex_common::types::{BlockBody, BlockHeader}; use ethrex_common::{Address, H256}; +use ethrex_common::{U256, types::EIP1559Transaction, types::Transaction, types::TxType}; use ethrex_crypto::NativeCrypto; -use ethrex_guest_program::l1::new_payload_request_to_block; +use ethrex_guest_program::l1::{new_payload_request_to_block, validate_public_keys}; use ethrex_l2::sequencer::native_rollup::l1_advancer::build_ssz_stateless_input; use libssz::SszDecode; @@ -202,3 +203,117 @@ fn block_to_ssz_to_block_preserves_hash() { "Block hash mismatch after SSZ round-trip" ); } + +/// A signed EIP-1559 transaction plus the address that signed it. +/// +/// Signed with raw secp256k1 rather than through `ethrex-rpc`'s `Signer`, whose +/// `sign_inplace` is async — the payload construction here is the same one +/// `impl Signable for EIP1559Transaction` uses (`0x02 || rlp_payload`). +fn signed_eip1559_tx(secret_bytes: [u8; 32], nonce: u64) -> (Transaction, Address) { + use ethrex_rlp::encode::PayloadRLPEncode as _; + + let secp = secp256k1::Secp256k1::new(); + let secret = secp256k1::SecretKey::from_byte_array(&secret_bytes).unwrap(); + + let mut tx = EIP1559Transaction { + chain_id: 1, + nonce, + max_priority_fee_per_gas: 1, + max_fee_per_gas: 10, + gas_limit: 21_000, + to: ethrex_common::types::TxKind::Call(Address::from_low_u64_be(0x1234)), + value: U256::from(1), + data: Bytes::new(), + access_list: vec![], + signature_y_parity: false, + signature_r: U256::zero(), + signature_s: U256::zero(), + ..Default::default() + }; + + let mut payload = vec![TxType::EIP1559 as u8]; + payload.append(&mut tx.encode_payload_to_vec()); + let msg = ethrex_common::utils::keccak(&payload); + + let (recovery_id, sig) = secp + .sign_ecdsa_recoverable(&secp256k1::Message::from_digest(msg.0), &secret) + .serialize_compact(); + tx.signature_r = U256::from_big_endian(&sig[..32]); + tx.signature_s = U256::from_big_endian(&sig[32..]); + tx.signature_y_parity = i32::from(recovery_id) != 0; + + let public_key = secret.public_key(&secp); + let hashed = ethrex_common::utils::keccak(&public_key.serialize_uncompressed()[1..]); + let address = Address::from_slice(&hashed[12..]); + + (Transaction::EIP1559Transaction(tx), address) +} + +/// The producer must emit one public key per transaction, in transaction order, +/// and each must be the uncompressed key of that transaction's signer. +/// +/// This is the producer half of the check the consumer performs in +/// `validate_public_keys` (#6716): before this, `build_ssz_stateless_input` sent +/// the list empty, so the consumer could not enforce the check at all. Two +/// transactions from *different* keys, so a swapped or repeated entry fails. +#[test] +fn producer_emits_one_matching_public_key_per_transaction() { + let (tx_a, addr_a) = signed_eip1559_tx([0x11; 32], 0); + let (tx_b, addr_b) = signed_eip1559_tx([0x22; 32], 0); + assert_ne!( + addr_a, addr_b, + "the two transactions must have distinct signers" + ); + + let (mut header, mut body) = make_test_block(); + body.transactions = vec![tx_a, tx_b]; + header.transactions_root = + ethrex_common::types::compute_transactions_root(&body.transactions, &NativeCrypto); + + let witness = ExecutionWitness { + codes: vec![], + block_headers_bytes: vec![], + first_block_number: 0, + chain_config: ethrex_common::types::ChainConfig { + chain_id: 1, + cancun_time: Some(0), + prague_time: Some(0), + ..Default::default() + }, + state_trie_root: None, + storage_trie_roots: Default::default(), + }; + + let ssz_bytes = + build_ssz_stateless_input(&header, &body, &witness, None).expect("SSZ encoding failed"); + let input = + ethrex_guest_program::l1::decode_stateless_input(&ssz_bytes).expect("SSZ decoding failed"); + + assert_eq!(input.public_keys.len(), 2, "one public key per transaction"); + for (expected, key) in [addr_a, addr_b].iter().zip(input.public_keys.iter()) { + let bytes: &[u8] = key; + assert_eq!(bytes.len(), 65, "keys are uncompressed secp256k1"); + assert_eq!(bytes[0], 0x04, "uncompressed keys are tagged 0x04"); + let hashed = ethrex_common::utils::keccak(&bytes[1..]); + assert_eq!( + Address::from_slice(&hashed[12..]), + *expected, + "key must derive to the transaction's signer" + ); + } + + // The consumer's check must accept what the producer emits. Reconstructing the + // block from the payload (rather than reusing `body`) is what the consumer + // actually does, so this exercises the real pair. + let block = new_payload_request_to_block(&input.new_payload_request, &NativeCrypto) + .expect("block reconstruction failed"); + validate_public_keys(&input.public_keys, &block, &NativeCrypto) + .expect("producer output must satisfy the consumer's public-key check"); + + // And it must reject a tampered list, so the acceptance above is meaningful. + let mut swapped: Vec<_> = input.public_keys.iter().cloned().collect(); + swapped.swap(0, 1); + let swapped = libssz_types::SszList::try_from(swapped).expect("public_keys fits"); + validate_public_keys(&swapped, &block, &NativeCrypto) + .expect_err("swapped public keys must be rejected"); +} From 2f4741fc801bb934d25663f4a2ea7db967540d0b Mon Sep 17 00:00:00 2001 From: Lucas Fiegl Date: Wed, 5 Aug 2026 16:24:05 -0300 Subject: [PATCH 18/30] ci: sign stateless-validator release assets --- .github/scripts/sign-stateless-artifacts.sh | 150 ++++++++++++++++++++ .github/workflows/pr_lint_gha.yaml | 115 +++++++++++++++ .github/workflows/tag_release.yaml | 29 ++++ docs/eip-8025.md | 79 ++++++++++- 4 files changed, 368 insertions(+), 5 deletions(-) create mode 100755 .github/scripts/sign-stateless-artifacts.sh diff --git a/.github/scripts/sign-stateless-artifacts.sh b/.github/scripts/sign-stateless-artifacts.sh new file mode 100755 index 00000000000..2f2f242aacd --- /dev/null +++ b/.github/scripts/sign-stateless-artifacts.sh @@ -0,0 +1,150 @@ +#!/usr/bin/env bash +# +# Sign the stateless-validator release artifacts with minisign. +# +# The zkEVM guest handbook requires the ELF and verification key to be released +# as signed assets from a public, open-source CI pipeline: +# https://github.com/eth-act/zkevm-standards/blob/main/handbooks/guest-handbook.md +# +# Mechanism and secret names match eth-act/ere-guests' own release pipeline +# (`compile-and-release.yml`), so anyone already verifying ere-guests artifacts +# can verify ours with the same command. +# +# Usage: sign-stateless-artifacts.sh +# +# Required environment: +# MINISIGN_SECRET_KEY minisign private key, as stored in the repo secret +# MINISIGN_PUBLIC_KEY the matching public key +# MINISIGN_PASSWORD password for the private key (empty for a `-W` key) +# +# Optional environment: +# TRUSTED_COMMENT_SUFFIX appended to each signed trusted comment, e.g. the tag +# and commit. Signed, so it is a provenance claim. +# COMMITTED_PUBLIC_KEY path to the in-repo public key (default +# .github/minisign.pub). Must exist and must match +# MINISIGN_PUBLIC_KEY — see "Why the committed key is +# mandatory" below. +set -euo pipefail + +ARTIFACT_DIR="${1:?usage: sign-stateless-artifacts.sh }" +COMMITTED_PUBLIC_KEY="${COMMITTED_PUBLIC_KEY:-.github/minisign.pub}" +TRUSTED_COMMENT_SUFFIX="${TRUSTED_COMMENT_SUFFIX:-}" + +: "${MINISIGN_SECRET_KEY:?MINISIGN_SECRET_KEY is not set}" +: "${MINISIGN_PUBLIC_KEY:?MINISIGN_PUBLIC_KEY is not set}" +MINISIGN_PASSWORD="${MINISIGN_PASSWORD:-}" + +if [ ! -d "$ARTIFACT_DIR" ]; then + echo "error: artifact directory '$ARTIFACT_DIR' does not exist" >&2 + exit 1 +fi + +# ── Key material ────────────────────────────────────────────────────────────── +# +# Why the committed key is mandatory: a public key shipped inside the same +# release it authenticates proves nothing, because anyone able to replace the +# artifacts can replace the key beside them. The signature is only meaningful +# against a key published out-of-band, so the in-repo copy is the source of +# truth and the released copy is a convenience. Asserting they match is what +# stops a rotated or mistyped secret from producing a release full of signatures +# that verify against nothing anyone has. + +if [ ! -f "$COMMITTED_PUBLIC_KEY" ]; then + cat >&2 < "$WORK_DIR/minisign.key" +printf '%s\n' "$MINISIGN_PUBLIC_KEY" > "$WORK_DIR/minisign.pub" + +if [ "$(key_line "$WORK_DIR/minisign.pub")" != "$(key_line "$COMMITTED_PUBLIC_KEY")" ]; then + cat >&2 <&2 + echo " expected files named stateless-validator-ethrex--.{elf,vk}" >&2 + find "$ARTIFACT_DIR" -type f | sort >&2 + exit 1 +fi + +echo "Signing ${#artifacts[@]} artifact(s) under '$ARTIFACT_DIR':" + +for file in "${artifacts[@]}"; do + filename="$(basename "$file")" + trusted_comment="$filename" + if [ -n "$TRUSTED_COMMENT_SUFFIX" ]; then + trusted_comment="$filename $TRUSTED_COMMENT_SUFFIX" + fi + + # The filename leads the trusted comment, matching ere-guests, so a consumer + # comparing it against the asset name still works. + printf '%s\n' "$MINISIGN_PASSWORD" | minisign \ + -S \ + -m "$file" \ + -s "$WORK_DIR/minisign.key" \ + -x "$file.minisig" \ + -t "$trusted_comment" \ + > /dev/null + + # Verify what was just produced, against the committed key rather than the + # secret's copy — this is the check a consumer will run, so running it here + # means a broken keypair fails the release instead of shipping. + # + # Overlaps with the key-match check above by design: that one fails fast with a + # precise diagnostic, this one is the last line of defence on the real release + # path, where nothing else verifies the output. + minisign -V -m "$file" -p "$COMMITTED_PUBLIC_KEY" -x "$file.minisig" > /dev/null + + echo " signed + verified: $filename" +done + +# Publish the committed key alongside the artifacts. It carries no authority by +# itself (see above) — it is there so a consumer who already trusts this +# repository does not have to fetch it separately. +# +# One directory deep, matching every other downloaded artifact, so the release +# job's `./bin/**/*` glob covers it without depending on whether `**` matches +# zero path segments in the uploader's glob implementation. +mkdir -p "$ARTIFACT_DIR/minisign" +cp "$COMMITTED_PUBLIC_KEY" "$ARTIFACT_DIR/minisign/minisign.pub" + +echo "Signed ${#artifacts[@]} artifact(s); public key written to $ARTIFACT_DIR/minisign/minisign.pub" diff --git a/.github/workflows/pr_lint_gha.yaml b/.github/workflows/pr_lint_gha.yaml index ebf872fed8d..b3eb4b46662 100644 --- a/.github/workflows/pr_lint_gha.yaml +++ b/.github/workflows/pr_lint_gha.yaml @@ -7,6 +7,9 @@ on: - ".github/**.yaml" - ".github/*.yml" - ".github/actions/**" + # Without this, editing a script under .github/scripts would not run the + # jobs below that exercise those scripts. + - ".github/scripts/**" permissions: contents: read @@ -64,3 +67,115 @@ jobs: check "final tag" tag v18.0.0 "" "release|18.0.0" check "main push" branch main "" "main|dev-deadbeef" check "PR build" branch main feat/x "feat/x|dev-deadbeef" + + stateless-signing: + name: sign-stateless-artifacts + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v6 + + - name: Install minisign + run: | + sudo apt-get update + sudo apt-get install -y minisign + + - name: Assert the signing script signs and refuses to no-op + env: + SCRIPT: .github/scripts/sign-stateless-artifacts.sh + run: | + set -euo pipefail + # Exercises the REAL script finalize-release runs, with a throwaway + # keypair. Its value is entirely in its guards — an unsigned release + # that still looks green is the failure it exists to prevent — so each + # guard is asserted to exit non-zero rather than merely to print. + work="$(mktemp -d)" + minisign -G -W -p "$work/pub" -s "$work/key" > /dev/null + minisign -G -W -p "$work/other.pub" -s "$work/other.key" > /dev/null + export MINISIGN_SECRET_KEY MINISIGN_PUBLIC_KEY + MINISIGN_SECRET_KEY="$(cat "$work/key")" + MINISIGN_PUBLIC_KEY="$(cat "$work/pub")" + + # Rebuild the layout download-artifact produces: one directory per + # artifact, including a non-stateless one that must be left alone. + make_tree() { + local root="$1" + rm -rf "$root" + for name in stateless-validator-ethrex-sp1-6.3.1 stateless-validator-ethrex-zisk-1.0.0-alpha; do + mkdir -p "$root/$name" + head -c 256 /dev/urandom > "$root/$name/$name.elf" + head -c 64 /dev/urandom > "$root/$name/$name.vk" + done + mkdir -p "$root/ethrex-linux-x86_64" + head -c 64 /dev/urandom > "$root/ethrex-linux-x86_64/ethrex" + } + + # Asserts the message too, not just a non-zero exit. Without that, a + # removed guard passes as long as *something* later happens to fail, + # which is how a guard silently stops being tested. + expect_fail() { # expect_fail

+ local out + out="$(mktemp)" + if COMMITTED_PUBLIC_KEY="$2" bash "$SCRIPT" "$3" > "$out" 2>&1; then + echo "::error::$1: script succeeded but must fail" + exit 1 + fi + if ! grep -qF "$4" "$out"; then + echo "::error::$1: failed for the wrong reason; expected '$4', got:" + cat "$out" + exit 1 + fi + echo "ok rejects: $1" + } + + # Happy path. + make_tree "$work/bin" + COMMITTED_PUBLIC_KEY="$work/pub" TRUSTED_COMMENT_SUFFIX="ethrex v0.0.0-test deadbeef" \ + bash "$SCRIPT" "$work/bin" > /dev/null + + signed=0 + for f in "$work"/bin/stateless-validator-*/*.elf "$work"/bin/stateless-validator-*/*.vk; do + minisign -V -m "$f" -p "$work/pub" -x "$f.minisig" > /dev/null + signed=$((signed + 1)) + done + if [ "$signed" -ne 4 ]; then + echo "::error::expected 4 signed artifacts, verified $signed" + exit 1 + fi + echo "ok signs and verifies 4 artifacts" + + # The public key must be published one directory deep, so the release + # job's ./bin/**/* glob covers it regardless of how ** treats an empty + # path segment. + test -f "$work/bin/minisign/minisign.pub" + echo "ok publishes minisign/minisign.pub" + + # Scoped to the stateless artifacts: the node binary is not signed. + if [ -e "$work/bin/ethrex-linux-x86_64/ethrex.minisig" ]; then + echo "::error::signed a non-stateless artifact" + exit 1 + fi + echo "ok leaves non-stateless artifacts unsigned" + + # A tampered artifact must stop verifying, proving the signature binds + # the bytes rather than just existing. + printf 'x' >> "$work/bin/stateless-validator-ethrex-sp1-6.3.1/stateless-validator-ethrex-sp1-6.3.1.elf" + if minisign -V -m "$work/bin/stateless-validator-ethrex-sp1-6.3.1/stateless-validator-ethrex-sp1-6.3.1.elf" \ + -p "$work/pub" > /dev/null 2>&1; then + echo "::error::a tampered artifact still verified" + exit 1 + fi + echo "ok tampered artifact fails verification" + + # Guards. + make_tree "$work/bin2" + expect_fail "a keypair that does not match the committed key" "$work/other.pub" "$work/bin2" \ + "does not match" + expect_fail "a missing committed public key" "$work/absent.pub" "$work/bin2" \ + "no committed public key" + mkdir -p "$work/empty/ethrex-linux-x86_64" + head -c 8 /dev/urandom > "$work/empty/ethrex-linux-x86_64/ethrex" + expect_fail "an artifact set with nothing to sign" "$work/pub" "$work/empty" \ + "no stateless-validator .elf/.vk artifacts found" + expect_fail "a missing artifact directory" "$work/pub" "$work/nonexistent" \ + "does not exist" diff --git a/.github/workflows/tag_release.yaml b/.github/workflows/tag_release.yaml index 141eb7a1dbe..9ba3df94cac 100644 --- a/.github/workflows/tag_release.yaml +++ b/.github/workflows/tag_release.yaml @@ -429,6 +429,10 @@ jobs: - publish-docker - package-contracts runs-on: ubuntu-latest + env: + # Signing is skipped entirely when the secret is absent, so forks and + # repos that have not configured a keypair still produce a release. + HAS_MINISIGN_SECRET_KEY: ${{ secrets.MINISIGN_SECRET_KEY != '' }} steps: - name: Checkout Code uses: actions/checkout@v6 @@ -444,6 +448,31 @@ jobs: # pattern would silently omit them from the release. pattern: "*ethrex*" + # The zkEVM guest handbook requires the stateless-validator ELF and + # verification key to ship as signed release assets: + # https://github.com/eth-act/zkevm-standards/blob/main/handbooks/guest-handbook.md + # + # This runs here rather than in the matrix build for two reasons: the + # signing key is exposed to one job instead of three, and this job needs + # `verify-stateless-validator-guest`, so an ELF that failed its + # conformance check is never signed. + - name: Install minisign + if: env.HAS_MINISIGN_SECRET_KEY == 'true' + run: | + sudo apt-get update + sudo apt-get install -y minisign + + - name: Sign stateless-validator artifacts + if: env.HAS_MINISIGN_SECRET_KEY == 'true' + env: + MINISIGN_SECRET_KEY: ${{ secrets.MINISIGN_SECRET_KEY }} + MINISIGN_PUBLIC_KEY: ${{ secrets.MINISIGN_PUBLIC_KEY }} + MINISIGN_PASSWORD: ${{ secrets.MINISIGN_PASSWORD }} + # Signed alongside the artifact, so the signature also attests which + # tag and commit produced it. + TRUSTED_COMMENT_SUFFIX: "ethrex ${{ github.ref_name }} ${{ github.sha }}" + run: .github/scripts/sign-stateless-artifacts.sh ./bin + - name: Get previous tag run: | last_tag=$(git --no-pager tag --sort=creatordate | grep -v -E '^v[0-9]+\.[0-9]+\.[0-9]+-' | grep -v '${{ github.ref_name }}' | tail -1) diff --git a/docs/eip-8025.md b/docs/eip-8025.md index a55ebe76b91..e481c88c1f6 100644 --- a/docs/eip-8025.md +++ b/docs/eip-8025.md @@ -183,9 +183,10 @@ to keep in step with the SDK. `tag_release.yaml` builds, keygens, verifies and attaches, per zkVM: ``` -stateless-validator-ethrex-zisk-1.0.0-alpha.elf + .vk -stateless-validator-ethrex-sp1-6.3.1.elf + .vk -stateless-validator-ethrex-openvm-2.0.0.elf + .vk +stateless-validator-ethrex-zisk-1.0.0-alpha.elf + .vk (+ .minisig each) +stateless-validator-ethrex-sp1-6.3.1.elf + .vk (+ .minisig each) +stateless-validator-ethrex-openvm-2.0.0.elf + .vk (+ .minisig each) +minisign.pub ``` Compilation runs in `ghcr.io/eth-act/ere/ere-compiler-`, and `.vk` comes from @@ -194,7 +195,76 @@ consume them. Version strings come from `.github/scripts/zkvm-version.sh`, which refuses to answer if the pinned `ere` rev in a bin manifest has moved — a rev bump cannot silently mislabel an asset. -`verify-stateless-validator-guest` then runs each freshly built ELF under +### Signing + +The [guest handbook][handbook] requires the ELF and verification key to ship as +signed release assets from a public, open-source CI pipeline. +`finalize-release` signs each with [minisign][minisign], producing a +`.minisig` beside every asset plus a `minisign.pub` in the release: + +``` +stateless-validator-ethrex--.elf + .elf.minisig +stateless-validator-ethrex--.vk + .vk.minisig +minisign.pub +``` + +Verify a downloaded asset with: + +```bash +minisign -Vm stateless-validator-ethrex-sp1-6.3.1.elf -p minisign.pub +``` + +The trusted comment — which is covered by the signature, unlike the untrusted +one — carries the asset name, tag and commit, so a verified signature also +attests which commit produced the artifact: + +``` +Trusted comment: stateless-validator-ethrex-sp1-6.3.1.elf ethrex v9.0.0-rc1 3f2a1c… +``` + +**Use `.github/minisign.pub` from this repository, not the copy in the release.** +A public key shipped inside the release it authenticates proves nothing: anyone +able to replace the assets can replace the key beside them. The in-repo copy is +the source of truth and the released copy is a convenience. +`sign-stateless-artifacts.sh` asserts the two match, so a rotated or mistyped +secret fails the release rather than shipping signatures that verify against a +key nobody has. + +Signing runs in `finalize-release` rather than in the matrix build so the key is +exposed to one job instead of three, and because that job needs +`verify-stateless-validator-guest` — an ELF that failed its conformance check is +never signed. The script also fails if it matches zero artifacts, since a version +bump or rename that moves the assets out from under its glob would otherwise +produce an unsigned release that still looks green. + +Mechanism and secret names match `eth-act/ere-guests`' own +`compile-and-release.yml`, so anyone already verifying ere-guests assets can +verify ours with the same command. + +**Repository setup** (one-time, by a maintainer with secret access): + +```bash +minisign -G -p minisign.pub -s minisign.key +``` + +| Secret | Contents | +|---|---| +| `MINISIGN_SECRET_KEY` | the full `minisign.key` | +| `MINISIGN_PUBLIC_KEY` | the full `minisign.pub` | +| `MINISIGN_PASSWORD` | the key's password (empty for a `-W` key) | + +Then commit `minisign.pub` to `.github/minisign.pub`. Until `MINISIGN_SECRET_KEY` +exists the signing steps are skipped and the release is unchanged, so forks are +unaffected; once it exists the committed key is mandatory. + +[handbook]: https://github.com/eth-act/zkevm-standards/blob/main/handbooks/guest-handbook.md +[minisign]: https://jedisct1.github.io/minisign/ + +--- + +## Conformance verification + +`verify-stateless-validator-guest` runs each freshly built ELF under `ere-server … execute` against a generated conformance vector and requires the returned `statelessOutputBytes` to match byte-for-byte. It deliberately uses a **true-success** vector: the root, `chain_id` and `schema_id` are all computed @@ -266,4 +336,3 @@ concerns. - The 6 vectors that differ beyond the root. - `eth-act/ere-guests` moving to the #3248+#3278 dialect; until it does, our artifacts are not drop-in for it. -- Minisign signing of release assets, as ere-guests does. From c9a4ade3eafc57ec1cf762f6bfcaf69ed63af34c Mon Sep 17 00:00:00 2001 From: Lucas Fiegl Date: Wed, 5 Aug 2026 16:51:52 -0300 Subject: [PATCH 19/30] ci: derive minisign pubkey from the secret key --- .github/scripts/sign-stateless-artifacts.sh | 99 +++++++++++++-------- .github/workflows/pr_lint_gha.yaml | 95 ++++++++++++-------- .github/workflows/tag_release.yaml | 3 +- docs/eip-8025.md | 37 +++++--- 4 files changed, 144 insertions(+), 90 deletions(-) diff --git a/.github/scripts/sign-stateless-artifacts.sh b/.github/scripts/sign-stateless-artifacts.sh index 2f2f242aacd..11ea592de53 100755 --- a/.github/scripts/sign-stateless-artifacts.sh +++ b/.github/scripts/sign-stateless-artifacts.sh @@ -14,16 +14,18 @@ # # Required environment: # MINISIGN_SECRET_KEY minisign private key, as stored in the repo secret -# MINISIGN_PUBLIC_KEY the matching public key # MINISIGN_PASSWORD password for the private key (empty for a `-W` key) # +# The public key is *derived* from the secret key with `minisign -R` rather than +# read from a third secret. That is one less secret to configure, and it makes a +# key that disagrees with the signing key impossible by construction. +# # Optional environment: # TRUSTED_COMMENT_SUFFIX appended to each signed trusted comment, e.g. the tag # and commit. Signed, so it is a provenance claim. # COMMITTED_PUBLIC_KEY path to the in-repo public key (default -# .github/minisign.pub). Must exist and must match -# MINISIGN_PUBLIC_KEY — see "Why the committed key is -# mandatory" below. +# .github/minisign.pub). When present it is the trust +# anchor and must match the derived key. set -euo pipefail ARTIFACT_DIR="${1:?usage: sign-stateless-artifacts.sh }" @@ -31,7 +33,6 @@ COMMITTED_PUBLIC_KEY="${COMMITTED_PUBLIC_KEY:-.github/minisign.pub}" TRUSTED_COMMENT_SUFFIX="${TRUSTED_COMMENT_SUFFIX:-}" : "${MINISIGN_SECRET_KEY:?MINISIGN_SECRET_KEY is not set}" -: "${MINISIGN_PUBLIC_KEY:?MINISIGN_PUBLIC_KEY is not set}" MINISIGN_PASSWORD="${MINISIGN_PASSWORD:-}" if [ ! -d "$ARTIFACT_DIR" ]; then @@ -40,48 +41,71 @@ if [ ! -d "$ARTIFACT_DIR" ]; then fi # ── Key material ────────────────────────────────────────────────────────────── -# -# Why the committed key is mandatory: a public key shipped inside the same -# release it authenticates proves nothing, because anyone able to replace the -# artifacts can replace the key beside them. The signature is only meaningful -# against a key published out-of-band, so the in-repo copy is the source of -# truth and the released copy is a convenience. Asserting they match is what -# stops a rotated or mistyped secret from producing a release full of signatures -# that verify against nothing anyone has. - -if [ ! -f "$COMMITTED_PUBLIC_KEY" ]; then - cat >&2 < "$WORK_DIR/minisign.key" + +# Derive the public key from the secret key. `minisign -R` reads the password on +# stdin exactly as `-S` does, and works for passwordless (`-G -W`) keys too. +if ! printf '%s\n' "$MINISIGN_PASSWORD" \ + | minisign -R -s "$WORK_DIR/minisign.key" -p "$WORK_DIR/minisign.pub" > /dev/null 2>&1; then + echo "error: could not derive the public key from MINISIGN_SECRET_KEY" >&2 + echo " (wrong MINISIGN_PASSWORD, or the secret is not a minisign key)" >&2 exit 1 fi # minisign key files are a comment line followed by the base64 key. Compare the -# key itself so an differing comment line is not treated as a mismatch. +# key itself, so a differing comment line is not treated as a mismatch. key_line() { grep -v '^untrusted comment:' "$1" | tr -d '[:space:]' } +DERIVED_KEY="$(key_line "$WORK_DIR/minisign.pub")" -WORK_DIR="$(mktemp -d)" -trap 'rm -rf "$WORK_DIR"' EXIT -umask 077 +# A public key shipped inside the same release it authenticates proves nothing: +# anyone able to replace the artifacts can replace the key beside them. The +# signature is only meaningful against a key published out-of-band, so the +# in-repo copy is the trust anchor and the released copy is a convenience. +# +# It is not yet required, because requiring it would fail the first release made +# after the signing secrets were configured. Once committed it is enforced: a +# mismatch is a hard failure, since it means consumers hold a key the release +# does not verify against. +PUBLIC_KEY_SOURCE="$WORK_DIR/minisign.pub" +if [ -f "$COMMITTED_PUBLIC_KEY" ]; then + if [ "$DERIVED_KEY" != "$(key_line "$COMMITTED_PUBLIC_KEY")" ]; then + cat >&2 <&2 < "$WORK_DIR/minisign.key" -printf '%s\n' "$MINISIGN_PUBLIC_KEY" > "$WORK_DIR/minisign.pub" +The release will carry a public key derived from MINISIGN_SECRET_KEY, but a key +published only inside the release it authenticates gives consumers nothing to +check it against. Commit this file as '$COMMITTED_PUBLIC_KEY' to make the +signatures meaningful; once it is there, this script enforces the match. -if [ "$(key_line "$WORK_DIR/minisign.pub")" != "$(key_line "$COMMITTED_PUBLIC_KEY")" ]; then - cat >&2 < /dev/null - # Verify what was just produced, against the committed key rather than the - # secret's copy — this is the check a consumer will run, so running it here + # Verify what was just produced, against the committed key when there is one — this is the check a consumer will run, so running it here # means a broken keypair fails the release instead of shipping. # # Overlaps with the key-match check above by design: that one fails fast with a # precise diagnostic, this one is the last line of defence on the real release # path, where nothing else verifies the output. - minisign -V -m "$file" -p "$COMMITTED_PUBLIC_KEY" -x "$file.minisig" > /dev/null + minisign -V -m "$file" -p "$PUBLIC_KEY_SOURCE" -x "$file.minisig" > /dev/null echo " signed + verified: $filename" done @@ -145,6 +168,6 @@ done # job's `./bin/**/*` glob covers it without depending on whether `**` matches # zero path segments in the uploader's glob implementation. mkdir -p "$ARTIFACT_DIR/minisign" -cp "$COMMITTED_PUBLIC_KEY" "$ARTIFACT_DIR/minisign/minisign.pub" +cp "$PUBLIC_KEY_SOURCE" "$ARTIFACT_DIR/minisign/minisign.pub" echo "Signed ${#artifacts[@]} artifact(s); public key written to $ARTIFACT_DIR/minisign/minisign.pub" diff --git a/.github/workflows/pr_lint_gha.yaml b/.github/workflows/pr_lint_gha.yaml index b3eb4b46662..6c26123fd0b 100644 --- a/.github/workflows/pr_lint_gha.yaml +++ b/.github/workflows/pr_lint_gha.yaml @@ -88,13 +88,15 @@ jobs: # Exercises the REAL script finalize-release runs, with a throwaway # keypair. Its value is entirely in its guards — an unsigned release # that still looks green is the failure it exists to prevent — so each - # guard is asserted to exit non-zero rather than merely to print. + # guard is asserted to fail with its OWN diagnostic, not merely to + # exit non-zero: a guard whose removal is masked by some later failure + # has silently stopped being tested. work="$(mktemp -d)" minisign -G -W -p "$work/pub" -s "$work/key" > /dev/null minisign -G -W -p "$work/other.pub" -s "$work/other.key" > /dev/null - export MINISIGN_SECRET_KEY MINISIGN_PUBLIC_KEY + printf 'pw\npw\n' | minisign -G -p "$work/pw.pub" -s "$work/pw.key" > /dev/null 2>&1 + export MINISIGN_SECRET_KEY MINISIGN_SECRET_KEY="$(cat "$work/key")" - MINISIGN_PUBLIC_KEY="$(cat "$work/pub")" # Rebuild the layout download-artifact produces: one directory per # artifact, including a non-stateless one that must be left alone. @@ -110,9 +112,18 @@ jobs: head -c 64 /dev/urandom > "$root/ethrex-linux-x86_64/ethrex" } - # Asserts the message too, not just a non-zero exit. Without that, a - # removed guard passes as long as *something* later happens to fail, - # which is how a guard silently stops being tested. + verify_all() { # verify_all + local n=0 f + for f in "$1"/stateless-validator-*/*.elf "$1"/stateless-validator-*/*.vk; do + minisign -V -m "$f" -p "$2" -x "$f.minisig" > /dev/null + n=$((n + 1)) + done + if [ "$n" -ne 4 ]; then + echo "::error::expected 4 signed artifacts, verified $n" + exit 1 + fi + } + expect_fail() { # expect_fail local out out="$(mktemp)" @@ -128,30 +139,34 @@ jobs: echo "ok rejects: $1" } - # Happy path. + # ── No committed public key ──────────────────────────────────────── + # The state the repository is in right now: the signing secrets exist + # but .github/minisign.pub has not been committed yet. This MUST still + # produce a signed release, or adding the secret would have broken the + # next tag. make_tree "$work/bin" - COMMITTED_PUBLIC_KEY="$work/pub" TRUSTED_COMMENT_SUFFIX="ethrex v0.0.0-test deadbeef" \ - bash "$SCRIPT" "$work/bin" > /dev/null - - signed=0 - for f in "$work"/bin/stateless-validator-*/*.elf "$work"/bin/stateless-validator-*/*.vk; do - minisign -V -m "$f" -p "$work/pub" -x "$f.minisig" > /dev/null - signed=$((signed + 1)) - done - if [ "$signed" -ne 4 ]; then - echo "::error::expected 4 signed artifacts, verified $signed" - exit 1 - fi - echo "ok signs and verifies 4 artifacts" - - # The public key must be published one directory deep, so the release - # job's ./bin/**/* glob covers it regardless of how ** treats an empty - # path segment. - test -f "$work/bin/minisign/minisign.pub" - echo "ok publishes minisign/minisign.pub" + out="$(mktemp)" + COMMITTED_PUBLIC_KEY="$work/absent.pub" TRUSTED_COMMENT_SUFFIX="ethrex v0.0.0-test deadbeef" \ + bash "$SCRIPT" "$work/bin" > "$out" 2>&1 + verify_all "$work/bin" "$work/pub" + grep -qF "::warning::No committed public key" "$out" || { + echo "::error::expected a warning about the missing committed key"; cat "$out"; exit 1; } + # The derived key must be printed, so a maintainer can commit it. + grep -qF "$(grep -v '^untrusted' "$work/pub")" "$out" || { + echo "::error::the derived public key was not printed"; cat "$out"; exit 1; } + diff <(grep -v '^untrusted' "$work/bin/minisign/minisign.pub") \ + <(grep -v '^untrusted' "$work/pub") > /dev/null + echo "ok signs with a derived key when none is committed, and says so" + + # ── Committed public key present ─────────────────────────────────── + make_tree "$work/bin2" + COMMITTED_PUBLIC_KEY="$work/pub" bash "$SCRIPT" "$work/bin2" > /dev/null + verify_all "$work/bin2" "$work/pub" + test -f "$work/bin2/minisign/minisign.pub" + echo "ok signs and verifies against a committed key" # Scoped to the stateless artifacts: the node binary is not signed. - if [ -e "$work/bin/ethrex-linux-x86_64/ethrex.minisig" ]; then + if [ -e "$work/bin2/ethrex-linux-x86_64/ethrex.minisig" ]; then echo "::error::signed a non-stateless artifact" exit 1 fi @@ -159,23 +174,27 @@ jobs: # A tampered artifact must stop verifying, proving the signature binds # the bytes rather than just existing. - printf 'x' >> "$work/bin/stateless-validator-ethrex-sp1-6.3.1/stateless-validator-ethrex-sp1-6.3.1.elf" - if minisign -V -m "$work/bin/stateless-validator-ethrex-sp1-6.3.1/stateless-validator-ethrex-sp1-6.3.1.elf" \ - -p "$work/pub" > /dev/null 2>&1; then + elf="$work/bin2/stateless-validator-ethrex-sp1-6.3.1/stateless-validator-ethrex-sp1-6.3.1.elf" + printf 'x' >> "$elf" + if minisign -V -m "$elf" -p "$work/pub" > /dev/null 2>&1; then echo "::error::a tampered artifact still verified" exit 1 fi echo "ok tampered artifact fails verification" - # Guards. - make_tree "$work/bin2" - expect_fail "a keypair that does not match the committed key" "$work/other.pub" "$work/bin2" \ + # ── Guards ───────────────────────────────────────────────────────── + make_tree "$work/bin3" + expect_fail "a committed key from a different keypair" "$work/other.pub" "$work/bin3" \ "does not match" - expect_fail "a missing committed public key" "$work/absent.pub" "$work/bin2" \ - "no committed public key" + expect_fail "an artifact set with nothing to sign" "$work/pub" "$work/empty-dir" \ + "does not exist" mkdir -p "$work/empty/ethrex-linux-x86_64" head -c 8 /dev/urandom > "$work/empty/ethrex-linux-x86_64/ethrex" - expect_fail "an artifact set with nothing to sign" "$work/pub" "$work/empty" \ + expect_fail "an artifact directory with no stateless assets" "$work/pub" "$work/empty" \ "no stateless-validator .elf/.vk artifacts found" - expect_fail "a missing artifact directory" "$work/pub" "$work/nonexistent" \ - "does not exist" + + # A wrong password must fail loudly rather than produce a key that + # silently disagrees with the one used to sign. + MINISIGN_SECRET_KEY="$(cat "$work/pw.key")" + MINISIGN_PASSWORD="wrong" expect_fail "a wrong key password" "$work/pw.pub" "$work/bin3" \ + "could not derive the public key" diff --git a/.github/workflows/tag_release.yaml b/.github/workflows/tag_release.yaml index 9ba3df94cac..6b51a668151 100644 --- a/.github/workflows/tag_release.yaml +++ b/.github/workflows/tag_release.yaml @@ -465,8 +465,9 @@ jobs: - name: Sign stateless-validator artifacts if: env.HAS_MINISIGN_SECRET_KEY == 'true' env: + # No MINISIGN_PUBLIC_KEY secret: the script derives the public key from + # the secret key, so the two cannot drift apart. MINISIGN_SECRET_KEY: ${{ secrets.MINISIGN_SECRET_KEY }} - MINISIGN_PUBLIC_KEY: ${{ secrets.MINISIGN_PUBLIC_KEY }} MINISIGN_PASSWORD: ${{ secrets.MINISIGN_PASSWORD }} # Signed alongside the artifact, so the signature also attests which # tag and commit produced it. diff --git a/docs/eip-8025.md b/docs/eip-8025.md index e481c88c1f6..9b6d7621d93 100644 --- a/docs/eip-8025.md +++ b/docs/eip-8025.md @@ -222,13 +222,19 @@ attests which commit produced the artifact: Trusted comment: stateless-validator-ethrex-sp1-6.3.1.elf ethrex v9.0.0-rc1 3f2a1c… ``` -**Use `.github/minisign.pub` from this repository, not the copy in the release.** -A public key shipped inside the release it authenticates proves nothing: anyone -able to replace the assets can replace the key beside them. The in-repo copy is -the source of truth and the released copy is a convenience. -`sign-stateless-artifacts.sh` asserts the two match, so a rotated or mistyped -secret fails the release rather than shipping signatures that verify against a -key nobody has. +**Prefer `.github/minisign.pub` from this repository over the copy in the +release.** A public key shipped inside the release it authenticates proves +nothing: anyone able to replace the assets can replace the key beside them. The +in-repo copy is the trust anchor; the released copy is a convenience. + +The public key is *derived* from `MINISIGN_SECRET_KEY` with `minisign -R`, not +read from a separate secret, so the published key cannot disagree with the key +that signed. When `.github/minisign.pub` is present the script requires it to +match the derived key and a mismatch fails the release, since it would mean +consumers hold a key the release does not verify against. When it is absent the +release still gets signed and the run logs a warning with the derived key to +commit — requiring it outright would fail the first release made after the +signing secrets were configured. Signing runs in `finalize-release` rather than in the matrix build so the key is exposed to one job instead of three, and because that job needs @@ -236,26 +242,31 @@ exposed to one job instead of three, and because that job needs never signed. The script also fails if it matches zero artifacts, since a version bump or rename that moves the assets out from under its glob would otherwise produce an unsigned release that still looks green. +`.github/workflows/pr_lint_gha.yaml` exercises the real script on every PR that +touches it, asserting each guard fails with its own diagnostic. Mechanism and secret names match `eth-act/ere-guests`' own `compile-and-release.yml`, so anyone already verifying ere-guests assets can verify ours with the same command. -**Repository setup** (one-time, by a maintainer with secret access): +**Repository setup.** `MINISIGN_SECRET_KEY` and `MINISIGN_PASSWORD` are already +configured; no `MINISIGN_PUBLIC_KEY` secret is needed. To create a keypair from +scratch: ```bash -minisign -G -p minisign.pub -s minisign.key +minisign -G -p minisign.pub -s minisign.key # -W for a passwordless key ``` | Secret | Contents | |---|---| | `MINISIGN_SECRET_KEY` | the full `minisign.key` | -| `MINISIGN_PUBLIC_KEY` | the full `minisign.pub` | | `MINISIGN_PASSWORD` | the key's password (empty for a `-W` key) | -Then commit `minisign.pub` to `.github/minisign.pub`. Until `MINISIGN_SECRET_KEY` -exists the signing steps are skipped and the release is unchanged, so forks are -unaffected; once it exists the committed key is mandatory. +The remaining step is to commit the public key to `.github/minisign.pub`. Take it +from the release job's log — it prints the derived key whenever that file is +missing — or from `minisign -R -s minisign.key -p minisign.pub` locally. Until +`MINISIGN_SECRET_KEY` exists the signing steps are skipped entirely, so forks are +unaffected. [handbook]: https://github.com/eth-act/zkevm-standards/blob/main/handbooks/guest-handbook.md [minisign]: https://jedisct1.github.io/minisign/ From 6eec36f86f59a9ee563a00febf83be134bb80abb Mon Sep 17 00:00:00 2001 From: Lucas Fiegl Date: Wed, 5 Aug 2026 17:33:31 -0300 Subject: [PATCH 20/30] ci: cross-check the minisign pubkey secret --- .github/scripts/sign-stateless-artifacts.sh | 65 +++++++++++++-------- .github/workflows/pr_lint_gha.yaml | 24 ++++++++ .github/workflows/tag_release.yaml | 6 +- docs/eip-8025.md | 43 ++++++++------ 4 files changed, 95 insertions(+), 43 deletions(-) diff --git a/.github/scripts/sign-stateless-artifacts.sh b/.github/scripts/sign-stateless-artifacts.sh index 11ea592de53..601513b9319 100755 --- a/.github/scripts/sign-stateless-artifacts.sh +++ b/.github/scripts/sign-stateless-artifacts.sh @@ -16,16 +16,20 @@ # MINISIGN_SECRET_KEY minisign private key, as stored in the repo secret # MINISIGN_PASSWORD password for the private key (empty for a `-W` key) # -# The public key is *derived* from the secret key with `minisign -R` rather than -# read from a third secret. That is one less secret to configure, and it makes a -# key that disagrees with the signing key impossible by construction. +# The public key is always *derived* from the secret key with `minisign -R`, so +# the key published with a release can never disagree with the key that signed +# it. Any other copy is treated as a claim to be checked against that, never as +# a substitute for it. # # Optional environment: # TRUSTED_COMMENT_SUFFIX appended to each signed trusted comment, e.g. the tag # and commit. Signed, so it is a provenance claim. +# MINISIGN_PUBLIC_KEY the public key as recorded in the repo secret. Not +# required — it is cross-checked against the derived +# key so a stale or mistyped secret is caught. # COMMITTED_PUBLIC_KEY path to the in-repo public key (default -# .github/minisign.pub). When present it is the trust -# anchor and must match the derived key. +# .github/minisign.pub). The out-of-band trust anchor; +# when present it must match the derived key. set -euo pipefail ARTIFACT_DIR="${1:?usage: sign-stateless-artifacts.sh }" @@ -64,32 +68,47 @@ key_line() { } DERIVED_KEY="$(key_line "$WORK_DIR/minisign.pub")" -# A public key shipped inside the same release it authenticates proves nothing: -# anyone able to replace the artifacts can replace the key beside them. The -# signature is only meaningful against a key published out-of-band, so the -# in-repo copy is the trust anchor and the released copy is a convenience. -# -# It is not yet required, because requiring it would fail the first release made -# after the signing secrets were configured. Once committed it is enforced: a -# mismatch is a hard failure, since it means consumers hold a key the release -# does not verify against. -PUBLIC_KEY_SOURCE="$WORK_DIR/minisign.pub" -if [ -f "$COMMITTED_PUBLIC_KEY" ]; then - if [ "$DERIVED_KEY" != "$(key_line "$COMMITTED_PUBLIC_KEY")" ]; then +# Cross-check any other recorded copy of the public key against the derived one. +# Neither is used *instead of* the derived key — the point is to catch a copy +# that has gone stale, which is a sign the key was rotated somewhere and not +# everywhere. +check_matches_derived() { # check_matches_derived + local recorded + recorded="$(key_line "$1")" + if [ "$DERIVED_KEY" != "$recorded" ]; then cat >&2 < "$WORK_DIR/secret-copy.pub" + check_matches_derived "$WORK_DIR/secret-copy.pub" "the MINISIGN_PUBLIC_KEY secret" + echo "Signing key matches the MINISIGN_PUBLIC_KEY secret" +fi + +# A public key shipped inside the same release it authenticates proves nothing: +# anyone able to replace the artifacts can replace the key beside them, and a +# repo secret is no better — it is not something a downloader can consult. The +# signature is only meaningful against a key published out-of-band, which is why +# the in-repo copy is the trust anchor and everything else is a convenience. +# +# It is not required, because requiring it would fail the first release made +# after the signing secrets were configured. Once committed it is enforced. +PUBLIC_KEY_SOURCE="$WORK_DIR/minisign.pub" +if [ -f "$COMMITTED_PUBLIC_KEY" ]; then + check_matches_derived "$COMMITTED_PUBLIC_KEY" "'$COMMITTED_PUBLIC_KEY'" echo "Signing key matches $COMMITTED_PUBLIC_KEY" PUBLIC_KEY_SOURCE="$COMMITTED_PUBLIC_KEY" else diff --git a/.github/workflows/pr_lint_gha.yaml b/.github/workflows/pr_lint_gha.yaml index 6c26123fd0b..3afa82796aa 100644 --- a/.github/workflows/pr_lint_gha.yaml +++ b/.github/workflows/pr_lint_gha.yaml @@ -182,6 +182,30 @@ jobs: fi echo "ok tampered artifact fails verification" + # ── MINISIGN_PUBLIC_KEY cross-check ──────────────────────────────── + # The secret is a recorded copy, not a substitute for the derived key: + # a matching one is confirmed, a stale one fails rather than being + # published in place of the real key. + make_tree "$work/bin-sec" + out="$(mktemp)" + MINISIGN_PUBLIC_KEY="$(cat "$work/pub")" COMMITTED_PUBLIC_KEY="$work/absent.pub" \ + bash "$SCRIPT" "$work/bin-sec" > "$out" 2>&1 + verify_all "$work/bin-sec" "$work/pub" + grep -qF "matches the MINISIGN_PUBLIC_KEY secret" "$out" || { + echo "::error::a matching MINISIGN_PUBLIC_KEY was not confirmed"; cat "$out"; exit 1; } + echo "ok confirms a matching MINISIGN_PUBLIC_KEY secret" + + make_tree "$work/bin-stale" + out="$(mktemp)" + if MINISIGN_PUBLIC_KEY="$(cat "$work/other.pub")" COMMITTED_PUBLIC_KEY="$work/absent.pub" \ + bash "$SCRIPT" "$work/bin-stale" > "$out" 2>&1; then + echo "::error::a stale MINISIGN_PUBLIC_KEY was accepted" + exit 1 + fi + grep -qF "the MINISIGN_PUBLIC_KEY secret" "$out" || { + echo "::error::stale secret failed for the wrong reason:"; cat "$out"; exit 1; } + echo "ok rejects: a stale MINISIGN_PUBLIC_KEY secret" + # ── Guards ───────────────────────────────────────────────────────── make_tree "$work/bin3" expect_fail "a committed key from a different keypair" "$work/other.pub" "$work/bin3" \ diff --git a/.github/workflows/tag_release.yaml b/.github/workflows/tag_release.yaml index 6b51a668151..f356e507ee6 100644 --- a/.github/workflows/tag_release.yaml +++ b/.github/workflows/tag_release.yaml @@ -465,10 +465,12 @@ jobs: - name: Sign stateless-validator artifacts if: env.HAS_MINISIGN_SECRET_KEY == 'true' env: - # No MINISIGN_PUBLIC_KEY secret: the script derives the public key from - # the secret key, so the two cannot drift apart. MINISIGN_SECRET_KEY: ${{ secrets.MINISIGN_SECRET_KEY }} MINISIGN_PASSWORD: ${{ secrets.MINISIGN_PASSWORD }} + # Not used to sign or to publish: the script derives the public key + # from the secret key, and cross-checks this against it so a stale + # secret is caught rather than trusted. + MINISIGN_PUBLIC_KEY: ${{ secrets.MINISIGN_PUBLIC_KEY }} # Signed alongside the artifact, so the signature also attests which # tag and commit produced it. TRUSTED_COMMENT_SUFFIX: "ethrex ${{ github.ref_name }} ${{ github.sha }}" diff --git a/docs/eip-8025.md b/docs/eip-8025.md index 9b6d7621d93..c655f6d7689 100644 --- a/docs/eip-8025.md +++ b/docs/eip-8025.md @@ -227,14 +227,23 @@ release.** A public key shipped inside the release it authenticates proves nothing: anyone able to replace the assets can replace the key beside them. The in-repo copy is the trust anchor; the released copy is a convenience. -The public key is *derived* from `MINISIGN_SECRET_KEY` with `minisign -R`, not -read from a separate secret, so the published key cannot disagree with the key -that signed. When `.github/minisign.pub` is present the script requires it to -match the derived key and a mismatch fails the release, since it would mean -consumers hold a key the release does not verify against. When it is absent the -release still gets signed and the run logs a warning with the derived key to -commit — requiring it outright would fail the first release made after the -signing secrets were configured. +The public key is always *derived* from `MINISIGN_SECRET_KEY` with +`minisign -R`, so the key published with a release can never disagree with the +key that signed it. Every other copy is treated as a claim to check against that, +never as a substitute: + +| Copy | Role | +|---|---| +| derived from the secret key | authoritative; what gets published | +| `MINISIGN_PUBLIC_KEY` secret | cross-checked; a stale value fails the release | +| `.github/minisign.pub` | the out-of-band trust anchor; cross-checked, and published in place of the derived copy when present | + +A repo secret is no better than the released copy as a trust anchor — a +downloader cannot consult it. Only the committed file gives consumers something +independent to verify against. It is not required, because requiring it would +fail the first release made after the signing secrets were configured; when it is +absent the release is still signed and the run logs a warning with the derived +key to commit. Signing runs in `finalize-release` rather than in the matrix build so the key is exposed to one job instead of three, and because that job needs @@ -249,22 +258,20 @@ Mechanism and secret names match `eth-act/ere-guests`' own `compile-and-release.yml`, so anyone already verifying ere-guests assets can verify ours with the same command. -**Repository setup.** `MINISIGN_SECRET_KEY` and `MINISIGN_PASSWORD` are already -configured; no `MINISIGN_PUBLIC_KEY` secret is needed. To create a keypair from -scratch: +**Repository setup.** `MINISIGN_SECRET_KEY`, `MINISIGN_PASSWORD` and +`MINISIGN_PUBLIC_KEY` are configured. To create a keypair from scratch: ```bash minisign -G -p minisign.pub -s minisign.key # -W for a passwordless key ``` -| Secret | Contents | -|---|---| -| `MINISIGN_SECRET_KEY` | the full `minisign.key` | -| `MINISIGN_PASSWORD` | the key's password (empty for a `-W` key) | +| Secret | Contents | Required | +|---|---|---| +| `MINISIGN_SECRET_KEY` | the full `minisign.key` | yes | +| `MINISIGN_PASSWORD` | the key's password (empty for a `-W` key) | yes | +| `MINISIGN_PUBLIC_KEY` | the full `minisign.pub` | no — cross-checked only | -The remaining step is to commit the public key to `.github/minisign.pub`. Take it -from the release job's log — it prints the derived key whenever that file is -missing — or from `minisign -R -s minisign.key -p minisign.pub` locally. Until +The remaining step is to commit the public key to `.github/minisign.pub`. Until `MINISIGN_SECRET_KEY` exists the signing steps are skipped entirely, so forks are unaffected. From 1a6d094dd2f2560e4ab2db8d99e001a57edbe70e Mon Sep 17 00:00:00 2001 From: Lucas Fiegl Date: Wed, 5 Aug 2026 17:46:14 -0300 Subject: [PATCH 21/30] ci: commit the minisign public key --- .github/minisign.pub | 2 ++ .github/workflows/pr_lint_gha.yaml | 21 +++++++++++++++++++++ docs/eip-8025.md | 18 ++++++++++-------- 3 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 .github/minisign.pub diff --git a/.github/minisign.pub b/.github/minisign.pub new file mode 100644 index 00000000000..f6ea9a0119b --- /dev/null +++ b/.github/minisign.pub @@ -0,0 +1,2 @@ +untrusted comment: minisign public key E074BFD13AAB8A02 +RWQCiqs60b904PZll0gXEAbQlLwdt7MXuitSIt2425a59ULS0NHpArDL diff --git a/.github/workflows/pr_lint_gha.yaml b/.github/workflows/pr_lint_gha.yaml index 3afa82796aa..8794fa1d79d 100644 --- a/.github/workflows/pr_lint_gha.yaml +++ b/.github/workflows/pr_lint_gha.yaml @@ -91,6 +91,27 @@ jobs: # guard is asserted to fail with its OWN diagnostic, not merely to # exit non-zero: a guard whose removal is masked by some later failure # has silently stopped being tested. + # The committed public key is the trust anchor consumers verify + # against, and the release now hard-fails if it does not match the + # signing key. A corrupted or truncated commit of it must therefore + # surface here, not at release time. + python3 - <<'EOF' + import base64, pathlib, sys + path = pathlib.Path(".github/minisign.pub") + if not path.is_file(): + sys.exit("::error::.github/minisign.pub is missing") + lines = [l for l in path.read_text().splitlines() if l.strip()] + if len(lines) != 2 or not lines[0].startswith("untrusted comment:"): + sys.exit("::error::.github/minisign.pub is not a minisign public key file") + raw = base64.b64decode(lines[1], validate=True) + if len(raw) != 42 or raw[:2] != b"Ed": + sys.exit(f"::error::.github/minisign.pub is not a 42-byte Ed25519 minisign key (got {len(raw)} bytes, algo {raw[:2]!r})") + key_id = format(int.from_bytes(raw[2:10], "little"), "016X") + if key_id not in lines[0]: + sys.exit(f"::error::.github/minisign.pub comment does not name its key id {key_id}") + print(f"ok committed public key is well-formed (key id {key_id})") + EOF + work="$(mktemp -d)" minisign -G -W -p "$work/pub" -s "$work/key" > /dev/null minisign -G -W -p "$work/other.pub" -s "$work/other.key" > /dev/null diff --git a/docs/eip-8025.md b/docs/eip-8025.md index c655f6d7689..d878b231415 100644 --- a/docs/eip-8025.md +++ b/docs/eip-8025.md @@ -236,14 +236,14 @@ never as a substitute: |---|---| | derived from the secret key | authoritative; what gets published | | `MINISIGN_PUBLIC_KEY` secret | cross-checked; a stale value fails the release | -| `.github/minisign.pub` | the out-of-band trust anchor; cross-checked, and published in place of the derived copy when present | +| `.github/minisign.pub` | the out-of-band trust anchor; cross-checked, and published in place of the derived copy | A repo secret is no better than the released copy as a trust anchor — a downloader cannot consult it. Only the committed file gives consumers something -independent to verify against. It is not required, because requiring it would -fail the first release made after the signing secrets were configured; when it is -absent the release is still signed and the run logs a warning with the derived -key to commit. +independent to verify against; it is key id `E074BFD13AAB8A02`. The file is not +*required* — when it is absent the release is still signed and the run logs a +warning with the derived key to commit — but with it present a signing key that +does not match it fails the release. Signing runs in `finalize-release` rather than in the matrix build so the key is exposed to one job instead of three, and because that job needs @@ -271,9 +271,11 @@ minisign -G -p minisign.pub -s minisign.key # -W for a passwordless key | `MINISIGN_PASSWORD` | the key's password (empty for a `-W` key) | yes | | `MINISIGN_PUBLIC_KEY` | the full `minisign.pub` | no — cross-checked only | -The remaining step is to commit the public key to `.github/minisign.pub`. Until -`MINISIGN_SECRET_KEY` exists the signing steps are skipped entirely, so forks are -unaffected. +The public key is committed at `.github/minisign.pub`; `pr_lint_gha.yaml` checks +it stays a well-formed 42-byte Ed25519 minisign key whose comment names its own +key id, so a corrupted commit of it fails in a PR rather than at release time. +Until `MINISIGN_SECRET_KEY` exists the signing steps are skipped entirely, so +forks are unaffected. [handbook]: https://github.com/eth-act/zkevm-standards/blob/main/handbooks/guest-handbook.md [minisign]: https://jedisct1.github.io/minisign/ From 42a40135d037d7e06b952247cbfcb8a087267d79 Mon Sep 17 00:00:00 2001 From: Lucas Fiegl Date: Wed, 5 Aug 2026 17:53:10 -0300 Subject: [PATCH 22/30] ci: add a release-asset signing dry run --- .github/actions/sign-stateless/action.yml | 51 +++++++++++ .github/workflows/tag_release.yaml | 103 +++++++++++++++++++--- docs/eip-8025.md | 13 +++ 3 files changed, 155 insertions(+), 12 deletions(-) create mode 100644 .github/actions/sign-stateless/action.yml diff --git a/.github/actions/sign-stateless/action.yml b/.github/actions/sign-stateless/action.yml new file mode 100644 index 00000000000..2368090c306 --- /dev/null +++ b/.github/actions/sign-stateless/action.yml @@ -0,0 +1,51 @@ +name: Sign stateless-validator artifacts +description: > + Install minisign and sign the stateless-validator ELFs and verification keys + in a directory, as required by the zkEVM guest handbook. + + Exists so the release job and the dry-run job cannot drift apart: a dry run + that signs differently from the real release is not testing the real release. + +inputs: + artifact-dir: + description: Directory to scan for stateless-validator .elf/.vk files. + required: true + secret-key: + description: minisign private key (the MINISIGN_SECRET_KEY secret). + required: true + password: + description: Password for the private key. Empty for a `-W` key. + required: false + default: "" + public-key: + description: > + Recorded public key (the MINISIGN_PUBLIC_KEY secret). Optional, and + cross-checked against the key derived from the private key rather than + used in its place. + required: false + default: "" + trusted-comment-suffix: + description: > + Appended to each signed trusted comment, e.g. the tag and commit. Covered + by the signature, so it is a provenance claim rather than a label. + required: false + default: "" + +runs: + using: composite + steps: + - name: Install minisign + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y minisign + + - name: Sign + shell: bash + env: + MINISIGN_SECRET_KEY: ${{ inputs.secret-key }} + MINISIGN_PASSWORD: ${{ inputs.password }} + MINISIGN_PUBLIC_KEY: ${{ inputs.public-key }} + TRUSTED_COMMENT_SUFFIX: ${{ inputs.trusted-comment-suffix }} + ARTIFACT_DIR: ${{ inputs.artifact-dir }} + run: .github/scripts/sign-stateless-artifacts.sh "$ARTIFACT_DIR" diff --git a/.github/workflows/tag_release.yaml b/.github/workflows/tag_release.yaml index f356e507ee6..c539ceb293a 100644 --- a/.github/workflows/tag_release.yaml +++ b/.github/workflows/tag_release.yaml @@ -419,6 +419,90 @@ jobs: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build-docker.outputs.tag }}-l2-arm64 # Creates a release on GitHub with the binaries + # Everything `finalize-release` does except publishing, on `workflow_dispatch`. + # + # The build and conformance-verify jobs above already run on every trigger, so + # before this the only part of the release path that could not be exercised + # without cutting a tag was the last one: download, sign, attach. That is also + # the part holding the signing key, so it is the part most worth rehearsing. + # + # Signs the REAL ELFs and verification keys from this run, with the REAL + # secrets, through the same composite action `finalize-release` uses — then + # uploads the signed set as a workflow artifact instead of creating a release. + dry-run-release-assets: + if: github.event_name == 'workflow_dispatch' + needs: + - build-stateless-validator-guest + - verify-stateless-validator-guest + runs-on: ubuntu-latest + env: + HAS_MINISIGN_SECRET_KEY: ${{ secrets.MINISIGN_SECRET_KEY != '' }} + steps: + - name: Checkout Code + uses: actions/checkout@v6 + + - name: Download artifacts + uses: actions/download-artifact@v6 + with: + path: ./bin + pattern: "*ethrex*" + + - name: Skip notice + if: env.HAS_MINISIGN_SECRET_KEY != 'true' + run: | + echo "::warning::MINISIGN_SECRET_KEY is not configured; nothing to rehearse." + + - name: Sign stateless-validator artifacts + if: env.HAS_MINISIGN_SECRET_KEY == 'true' + uses: ./.github/actions/sign-stateless + with: + artifact-dir: ./bin + secret-key: ${{ secrets.MINISIGN_SECRET_KEY }} + password: ${{ secrets.MINISIGN_PASSWORD }} + public-key: ${{ secrets.MINISIGN_PUBLIC_KEY }} + # Marked as a rehearsal so a signature produced here can never be + # mistaken for one covering a released artifact. + trusted-comment-suffix: "ethrex DRY-RUN ${{ github.ref_name }} ${{ github.sha }}" + + # Verify independently of the script, against the committed key rather + # than anything this run produced — the check a downloader would perform. + - name: Verify every signature as a consumer would + if: env.HAS_MINISIGN_SECRET_KEY == 'true' + run: | + set -euo pipefail + count=0 + while IFS= read -r -d '' file; do + minisign -V -m "$file" -p .github/minisign.pub -x "$file.minisig" + count=$((count + 1)) + done < <(find ./bin -type f -name 'stateless-validator-ethrex-*' \ + \( -name '*.elf' -o -name '*.vk' \) -print0) + if [ "$count" -eq 0 ]; then + echo "::error::no stateless-validator artifacts were verified" + exit 1 + fi + echo "Verified $count signature(s) against the committed public key." + + - name: Summarise + if: env.HAS_MINISIGN_SECRET_KEY == 'true' + run: | + { + echo "### Release-asset dry run" + echo + echo "Signed and verified against \`.github/minisign.pub\`; nothing was published." + echo + echo '```' + find ./bin -type f \( -name '*.minisig' -o -name 'minisign.pub' \) | sort + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + + - name: Upload the signed set + if: env.HAS_MINISIGN_SECRET_KEY == 'true' + uses: actions/upload-artifact@v6 + with: + name: dry-run-signed-release-assets + path: ./bin + if-no-files-found: error + finalize-release: if: github.ref_type == 'tag' && github.event_name != 'workflow_dispatch' needs: @@ -456,25 +540,20 @@ jobs: # signing key is exposed to one job instead of three, and this job needs # `verify-stateless-validator-guest`, so an ELF that failed its # conformance check is never signed. - - name: Install minisign - if: env.HAS_MINISIGN_SECRET_KEY == 'true' - run: | - sudo apt-get update - sudo apt-get install -y minisign - - name: Sign stateless-validator artifacts if: env.HAS_MINISIGN_SECRET_KEY == 'true' - env: - MINISIGN_SECRET_KEY: ${{ secrets.MINISIGN_SECRET_KEY }} - MINISIGN_PASSWORD: ${{ secrets.MINISIGN_PASSWORD }} + uses: ./.github/actions/sign-stateless + with: + artifact-dir: ./bin + secret-key: ${{ secrets.MINISIGN_SECRET_KEY }} + password: ${{ secrets.MINISIGN_PASSWORD }} # Not used to sign or to publish: the script derives the public key # from the secret key, and cross-checks this against it so a stale # secret is caught rather than trusted. - MINISIGN_PUBLIC_KEY: ${{ secrets.MINISIGN_PUBLIC_KEY }} + public-key: ${{ secrets.MINISIGN_PUBLIC_KEY }} # Signed alongside the artifact, so the signature also attests which # tag and commit produced it. - TRUSTED_COMMENT_SUFFIX: "ethrex ${{ github.ref_name }} ${{ github.sha }}" - run: .github/scripts/sign-stateless-artifacts.sh ./bin + trusted-comment-suffix: "ethrex ${{ github.ref_name }} ${{ github.sha }}" - name: Get previous tag run: | diff --git a/docs/eip-8025.md b/docs/eip-8025.md index d878b231415..9c9ce8b5d79 100644 --- a/docs/eip-8025.md +++ b/docs/eip-8025.md @@ -245,6 +245,19 @@ independent to verify against; it is key id `E074BFD13AAB8A02`. The file is not warning with the derived key to commit — but with it present a signing key that does not match it fails the release. +**Rehearsing it.** `workflow_dispatch` on `tag_release.yaml` runs the whole +stateless path without cutting a release: the build and conformance-verify jobs +already run on every trigger, and `dry-run-release-assets` then does what +`finalize-release` does — download, sign the real ELFs and verification keys with +the real secrets through the same composite action, verify each signature against +the committed key — but uploads the signed set as a workflow artifact instead of +publishing. Its trusted comments carry `DRY-RUN`, so a rehearsal signature can +never be mistaken for one covering a released artifact. + +```bash +gh workflow run tag_release.yaml --ref +``` + Signing runs in `finalize-release` rather than in the matrix build so the key is exposed to one job instead of three, and because that job needs `verify-stateless-validator-guest` — an ELF that failed its conformance check is From 3b251eae8b7e25fc46adb4e4fc1922fdea65c452 Mon Sep 17 00:00:00 2001 From: Lucas Fiegl Date: Thu, 6 Aug 2026 09:49:23 -0300 Subject: [PATCH 23/30] ci: don't write GHCR cache on manual dispatch --- .github/workflows/tag_release.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/tag_release.yaml b/.github/workflows/tag_release.yaml index c539ceb293a..ca84610fe20 100644 --- a/.github/workflows/tag_release.yaml +++ b/.github/workflows/tag_release.yaml @@ -369,6 +369,13 @@ jobs: dockerhub_username: ${{ vars.DOCKERHUB_USERNAME }} dockerhub_password: ${{ secrets.DOCKERHUB_TOKEN }} push: ${{ github.event_name != 'workflow_dispatch'}} + # A manual dispatch must not write shared state. `push` alone is not + # enough: buildkit's `cache-to` writes cache layers to the registry + # independently of it, and for any ref that is neither main nor a PR + # the cache scope resolves to `main` — so a dispatch from a feature + # branch would overwrite main's cache tags. Falling back to the GHA + # cache keeps the build warm without touching GHCR. + cache_write: ${{ github.event_name != 'workflow_dispatch' }} tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.TAG_VERSION }}-${{ matrix.arch }} platforms: linux/${{ matrix.arch }} variant: l1 @@ -383,6 +390,13 @@ jobs: dockerhub_username: ${{ vars.DOCKERHUB_USERNAME }} dockerhub_password: ${{ secrets.DOCKERHUB_TOKEN }} push: ${{ github.event_name != 'workflow_dispatch'}} + # A manual dispatch must not write shared state. `push` alone is not + # enough: buildkit's `cache-to` writes cache layers to the registry + # independently of it, and for any ref that is neither main nor a PR + # the cache scope resolves to `main` — so a dispatch from a feature + # branch would overwrite main's cache tags. Falling back to the GHA + # cache keeps the build warm without touching GHCR. + cache_write: ${{ github.event_name != 'workflow_dispatch' }} tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.TAG_VERSION }}-l2-${{ matrix.arch }} build_args: BUILD_FLAGS=--features l2,l2-sql platforms: linux/${{ matrix.arch }} From 373b1da2b5a0b7db14bd75263dced63fda5e9580 Mon Sep 17 00:00:00 2001 From: Lucas Fiegl Date: Thu, 6 Aug 2026 10:07:53 -0300 Subject: [PATCH 24/30] fix(guest): declare alloc for zkVM crypto providers --- .github/workflows/pr_nostd.yaml | 36 +++++++++++++++++++ .../stateless-validator/src/lib.rs | 5 +++ 2 files changed, 41 insertions(+) diff --git a/.github/workflows/pr_nostd.yaml b/.github/workflows/pr_nostd.yaml index 4fba42c498e..b12bbd0c3e9 100644 --- a/.github/workflows/pr_nostd.yaml +++ b/.github/workflows/pr_nostd.yaml @@ -9,6 +9,7 @@ on: paths: - "crates/common/**" - "crates/vm/**" + - "crates/guest-program/**" - ".github/targets/**" - ".github/workflows/pr_nostd.yaml" @@ -48,3 +49,38 @@ jobs: -p ethrex-rlp -p ethrex-crypto -p ethrex-trie --no-default-features --target "$TARGET" cargo +nightly-2026-06-29 -Z json-target-spec -Z build-std=core,alloc check \ -p ethrex-crypto --no-default-features --features kzg-rs --target "$TARGET" + + stateless-validator-features: + # The stateless-validator guest is its own workspace, so the main CI's + # `cargo check --workspace` never sees it, and its zkVM crypto providers are + # behind features that a host build does not enable. Before this job the + # first thing to compile `crypto/zkvm_interface.rs` was the Ere docker build + # in tag_release — so a missing `extern crate alloc` cost a full guest build + # to discover. These checks take a couple of minutes. + name: stateless-validator feature check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Setup Rust Environment + uses: ./.github/actions/setup-rust + + - name: Check each feature combination + working-directory: crates/guest-program/stateless-validator + run: | + set -euo pipefail + # `host` alone does not compile either crypto provider, which is + # exactly why it missed the alloc regression — check the providers too. + for features in host ere zkvm-interface host,ere; do + echo "::group::cargo check --features $features" + cargo check --features "$features" --all-targets + echo "::endgroup::" + done + + # openvm is deliberately absent: its crates require a newer rustc than + # the pinned toolchain, and the openvm bin unifies the openvm source id + # with a `[patch]` that this crate does not carry, so a check here + # resolves different versions than the real build. Only the Ere docker + # build in tag_release checks it faithfully. `extern crate alloc` is + # crate-level, so the zkvm-interface check above still guards the + # regression that affected `crypto/openvm.rs`. diff --git a/crates/guest-program/stateless-validator/src/lib.rs b/crates/guest-program/stateless-validator/src/lib.rs index feadf97272a..def32dd5f7f 100644 --- a/crates/guest-program/stateless-validator/src/lib.rs +++ b/crates/guest-program/stateless-validator/src/lib.rs @@ -20,6 +20,11 @@ //! Target: execution-specs `3c3b6f4af315b268a61e20d5a4da8aa4f24c91f0` //! (#3248 progressive SSZ + #3278 `ChainConfig` removal). +// The zkVM crypto providers are written against `alloc` rather than `std`, +// because a guest may be built without std. Declaring the crate here makes those +// imports resolve in either configuration; it is a no-op for host builds. +extern crate alloc; + #[cfg(any(feature = "ere", feature = "zkvm-interface", feature = "openvm"))] pub mod crypto; #[cfg(feature = "ere")] From 790d884bd0f4e167906864347bd33bf4ef0353cf Mon Sep 17 00:00:00 2001 From: Lucas Fiegl Date: Thu, 6 Aug 2026 10:07:53 -0300 Subject: [PATCH 25/30] deps: libssz 0.3.0 with the progressive fix --- Cargo.lock | 20 ++++++++----------- Cargo.toml | 11 ++++++---- .../stateless-validator/Cargo.lock | 20 ++++++++----------- .../stateless-validator/Cargo.toml | 9 ++++++--- test/tests/common/progressive_ssz_tests.rs | 18 ++++++++--------- 5 files changed, 38 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 56a4a23d31c..8c8c2d04abb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6699,18 +6699,16 @@ dependencies = [ [[package]] name = "libssz" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d498c0482bba87d2647ea4601ea76cf2b498065e3958798a88f49274f3ced5e9" +version = "0.3.0" +source = "git+https://github.com/lambdaclass/libssz?branch=update-consensus-specs-v1.7.0-alpha.13#de932604d53b088a0e9990077d808942fa29ca29" dependencies = [ "smallvec", ] [[package]] name = "libssz-derive" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08ddfb5c969c28a4a54043e630f80c723352637bd1020f256ee3ac7a8814922b" +version = "0.3.0" +source = "git+https://github.com/lambdaclass/libssz?branch=update-consensus-specs-v1.7.0-alpha.13#de932604d53b088a0e9990077d808942fa29ca29" dependencies = [ "proc-macro2", "quote", @@ -6719,9 +6717,8 @@ dependencies = [ [[package]] name = "libssz-merkle" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63c6d6d5ce5d79bba66bc98c99869eedffedf7f14f0aa0915f1a62802650bdf6" +version = "0.3.0" +source = "git+https://github.com/lambdaclass/libssz?branch=update-consensus-specs-v1.7.0-alpha.13#de932604d53b088a0e9990077d808942fa29ca29" dependencies = [ "libssz", "sha2", @@ -6729,9 +6726,8 @@ dependencies = [ [[package]] name = "libssz-types" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747273ab2d923e82ed147091fe0fb3e602dd2012c872cdad5efe69e27c3b4099" +version = "0.3.0" +source = "git+https://github.com/lambdaclass/libssz?branch=update-consensus-specs-v1.7.0-alpha.13#de932604d53b088a0e9990077d808942fa29ca29" dependencies = [ "libssz", "libssz-merkle", diff --git a/Cargo.toml b/Cargo.toml index c6befb3c123..a3b012d9132 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -151,10 +151,13 @@ rocksdb = { version = "0.24.0", default-features = false, features = [ ] } # libssz (EIP-8025 SSZ support) -libssz = "0.2.2" -libssz-types = "0.2.2" -libssz-merkle = "0.2.2" -libssz-derive = "0.2.2" +# Pinned to the branch carrying the EIP-7916 progressive child-order fix +# (subtree left, remainder right). 0.2.2 had them swapped, which made every +# progressive hash_tree_root disagree with remerkleable and the spec fixtures. +libssz = { git = "https://github.com/lambdaclass/libssz", branch = "update-consensus-specs-v1.7.0-alpha.13" } +libssz-types = { git = "https://github.com/lambdaclass/libssz", branch = "update-consensus-specs-v1.7.0-alpha.13" } +libssz-merkle = { git = "https://github.com/lambdaclass/libssz", branch = "update-consensus-specs-v1.7.0-alpha.13" } +libssz-derive = { git = "https://github.com/lambdaclass/libssz", branch = "update-consensus-specs-v1.7.0-alpha.13" } [workspace.lints.clippy] redundant_clone = "warn" diff --git a/crates/guest-program/stateless-validator/Cargo.lock b/crates/guest-program/stateless-validator/Cargo.lock index 774f2aba3d2..c16d3780bd9 100644 --- a/crates/guest-program/stateless-validator/Cargo.lock +++ b/crates/guest-program/stateless-validator/Cargo.lock @@ -1632,18 +1632,16 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libssz" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d498c0482bba87d2647ea4601ea76cf2b498065e3958798a88f49274f3ced5e9" +version = "0.3.0" +source = "git+https://github.com/lambdaclass/libssz?branch=update-consensus-specs-v1.7.0-alpha.13#de932604d53b088a0e9990077d808942fa29ca29" dependencies = [ "smallvec", ] [[package]] name = "libssz-derive" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08ddfb5c969c28a4a54043e630f80c723352637bd1020f256ee3ac7a8814922b" +version = "0.3.0" +source = "git+https://github.com/lambdaclass/libssz?branch=update-consensus-specs-v1.7.0-alpha.13#de932604d53b088a0e9990077d808942fa29ca29" dependencies = [ "proc-macro2", "quote", @@ -1652,9 +1650,8 @@ dependencies = [ [[package]] name = "libssz-merkle" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63c6d6d5ce5d79bba66bc98c99869eedffedf7f14f0aa0915f1a62802650bdf6" +version = "0.3.0" +source = "git+https://github.com/lambdaclass/libssz?branch=update-consensus-specs-v1.7.0-alpha.13#de932604d53b088a0e9990077d808942fa29ca29" dependencies = [ "libssz", "sha2", @@ -1662,9 +1659,8 @@ dependencies = [ [[package]] name = "libssz-types" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747273ab2d923e82ed147091fe0fb3e602dd2012c872cdad5efe69e27c3b4099" +version = "0.3.0" +source = "git+https://github.com/lambdaclass/libssz?branch=update-consensus-specs-v1.7.0-alpha.13#de932604d53b088a0e9990077d808942fa29ca29" dependencies = [ "libssz", "libssz-merkle", diff --git a/crates/guest-program/stateless-validator/Cargo.toml b/crates/guest-program/stateless-validator/Cargo.toml index 353b70ccd05..286bb61101d 100644 --- a/crates/guest-program/stateless-validator/Cargo.toml +++ b/crates/guest-program/stateless-validator/Cargo.toml @@ -19,9 +19,12 @@ ethrex-guest-program = { path = "../", default-features = false } # SSZ encode/decode and merkleization. The mirror spike reached these through # eth-act's `stateless-validator-common`; ethrex owns the wire types natively, so # they are declared here directly. Versions match the root workspace. -libssz = "0.2.2" -libssz-merkle = "0.2.2" -libssz-types = "0.2.2" +# Must track the root workspace exactly: these are the same containers the host +# encodes, and two libssz versions in one binary would have the guest merkleize +# with a different progressive child order than ethrex-common does. +libssz = { git = "https://github.com/lambdaclass/libssz", branch = "update-consensus-specs-v1.7.0-alpha.13" } +libssz-merkle = { git = "https://github.com/lambdaclass/libssz", branch = "update-consensus-specs-v1.7.0-alpha.13" } +libssz-types = { git = "https://github.com/lambdaclass/libssz", branch = "update-consensus-specs-v1.7.0-alpha.13" } # ere platform abstraction (entrypoint / IO / cycle scopes) used by the per-zkVM # bins. Pinned to one ere rev; the ere-compiler and ere-server image tags in diff --git a/test/tests/common/progressive_ssz_tests.rs b/test/tests/common/progressive_ssz_tests.rs index a2f6a2e9035..7874b2fea9d 100644 --- a/test/tests/common/progressive_ssz_tests.rs +++ b/test/tests/common/progressive_ssz_tests.rs @@ -63,8 +63,13 @@ const REMERKLEABLE_UINT64_ROOTS: &[(usize, &str)] = &[ ), ]; -/// KNOWN FAILING — `libssz-merkle 0.2.2` has the progressive subtree children -/// swapped relative to the spec, so every progressive root it computes is wrong. +/// Guards the EIP-7916 progressive child order against `remerkleable`. +/// +/// This failed against `libssz-merkle 0.2.2`, which had the two children of each +/// progressive subtree swapped, making every progressive root ethrex computed +/// disagree with the spec. Fixed upstream in libssz 0.3.0; the root workspace +/// pins the branch carrying it. Keep this test passing rather than deleting it: +/// it is what pins the pin. /// /// `merkleize_progressive_inner` (libssz-merkle-0.2.2/src/lib.rs:151) ends with /// `hash_nodes(hasher, &rest, &subtree)` — remainder left, subtree right — and a @@ -90,13 +95,9 @@ const REMERKLEABLE_UINT64_ROOTS: &[(usize, &str)] = &[ /// remerkleable's `depth + 2`); only the child order differs. The fix is to swap /// that one `hash_nodes` argument pair in `libssz-merkle`. /// -/// This blocks adopting execution-specs #3248: ethrex cannot compute a correct -/// `new_payload_request_root` until it is fixed, because `SszExecutionPayload` -/// and `SszExecutionRequests` are progressive containers and every payload's -/// root flows through this function. Remove `#[ignore]` once `libssz` is fixed — -/// the assertion is already correct. +/// `SszExecutionPayload` and `SszExecutionRequests` are progressive containers, +/// so every `new_payload_request_root` the guest commits flows through this. #[test] -#[ignore = "libssz-merkle 0.2.2 swaps progressive subtree children; see doc comment"] fn progressive_list_roots_match_remerkleable() { let hasher = CryptoHasher(NativeCrypto); @@ -136,7 +137,6 @@ fn progressive_list_roots_match_remerkleable() { /// PC(active_fields=[1;5], a=1, b=2, c=3, d=4, e=5) -> 5a167eaf… /// ``` #[test] -#[ignore = "libssz-merkle 0.2.2 swaps progressive subtree children; see doc comment"] fn progressive_container_roots_match_remerkleable() { use libssz_merkle::{Node, merkleize_progressive, mix_in_active_fields}; From 08b09afbdb15e7ee366f9d916b197109786a150e Mon Sep 17 00:00:00 2001 From: Lucas Fiegl Date: Thu, 6 Aug 2026 10:09:56 -0300 Subject: [PATCH 26/30] test(guest): report which output field diverged --- .../tests/host_fixtures.rs | 86 ++++++++++++++++--- 1 file changed, 73 insertions(+), 13 deletions(-) diff --git a/crates/guest-program/stateless-validator/tests/host_fixtures.rs b/crates/guest-program/stateless-validator/tests/host_fixtures.rs index ce06100d15b..1961c802534 100644 --- a/crates/guest-program/stateless-validator/tests/host_fixtures.rs +++ b/crates/guest-program/stateless-validator/tests/host_fixtures.rs @@ -13,19 +13,33 @@ //! stateless-vector` run — not its parent, which also holds a `.meta/index.json` //! that is not a fixture. //! -//! MEASURED BASELINE, 2026-08-05, against the 769-block generated vector set: -//! 8 exact matches, 755 differing **only** in bytes 0..32 -//! (`new_payload_request_root`), 6 differing more widely. +//! MEASURED BASELINE, 2026-08-06, against the 768-block generated vector set, +//! with libssz 0.3.0 (the EIP-7916 progressive child-order fix): **762 exact +//! matches, 6 differing — all of them in `successful_validation` only**. //! -//! The 755 are all explained by one upstream defect: `libssz-merkle 0.2.2` -//! reverses the progressive-merkleization subtree children, so every -//! `hash_tree_root` over a `ProgressiveContainer` is wrong. On those blocks -//! `successful_validation`, `chain_id` and `schema_id` are already byte-identical -//! to the reference — including on true-success cases — so decode, witness -//! rebuild, public-key validation, block reconstruction and execution all agree -//! with execution-specs today. See `test/tests/common/progressive_ssz_tests.rs` -//! for the proof and the one-line fix. Expect ~763/769 once it lands; the -//! remaining 6 need separate investigation. +//! Every root now matches, so decode, witness rebuild, public-key validation, +//! block reconstruction, merkleization and encoding all agree with +//! execution-specs. What is left is six genuine disagreements about whether a +//! block is valid. Under libssz 0.2.2 this was 8 exact / 755 root-only / 6, and +//! the 755 were entirely the reversed progressive subtree children; see +//! `test/tests/common/progressive_ssz_tests.rs`. +//! +//! Five are ethrex being too strict — the spec accepts, we reject: +//! - `test_witness_7702::test_witness_codes_auth_nonce_mismatch` +//! - `test_witness_7702::test_witness_codes_redelegation_old_marker_included_new_marker_excluded` +//! - `test_witness_7702::test_witness_codes_reset_delegation` +//! - `test_witness_bytecodes_contract_creation::test_witness_codes_failed_create_after_initcode_read` +//! - `test_witness_validation_state::test_validation_state_extra_unused_trie_node` +//! +//! One is ethrex being too lax, which is the one that matters — the spec +//! rejects, we accept: +//! - `test_witness_validation_headers::test_validation_headers_non_contiguous_chain` (block5) +//! +//! A guest that accepts a payload the spec rejects can prove an invalid state +//! transition, so the non-contiguous-chain case is a correctness bug rather than +//! a conformance gap. Tracked separately; this test stays red until all six are +//! resolved rather than being pinned to the current count, so no regression can +//! hide behind an expected-failure list. #![cfg(feature = "host")] mod common; @@ -57,7 +71,11 @@ fn eest_fixture_equivalence() { for fixture in &fixtures { let output = run_stateless_validation(&fixture.stateless_input_bytes, crypto.clone()); if output != fixture.stateless_output_bytes { - failures.push(fixture.name.clone()); + failures.push(format!( + "{}\n {}", + fixture.name, + describe_divergence(&output, &fixture.stateless_output_bytes) + )); } } assert!( @@ -69,3 +87,45 @@ fn eest_fixture_equivalence() { ); println!("{} fixtures matched expected output bytes", fixtures.len()); } + +/// Name the diverging fields of an `SszStatelessValidationResult`. +/// +/// The output is a fixed 43-byte layout: root[0..32], successful_validation[32], +/// chain_id[33..41], schema_id[41..43]. Which field differs says what kind of +/// bug it is — a root-only difference is an encoding or merkleization problem, +/// whereas `successful_validation` is a disagreement about the block itself. +fn describe_divergence(got: &[u8], want: &[u8]) -> String { + if got.len() != want.len() { + return format!("length {} != expected {}", got.len(), want.len()); + } + let mut parts = Vec::new(); + if got[..32] != want[..32] { + parts.push(format!( + "root {} != {}", + hex::encode(&got[..32]), + hex::encode(&want[..32]) + )); + } + if got[32] != want[32] { + parts.push(format!( + "successful_validation {} != {}", + got[32] != 0, + want[32] != 0 + )); + } + if got[33..41] != want[33..41] { + parts.push(format!( + "chain_id {} != {}", + u64::from_le_bytes(got[33..41].try_into().expect("8 bytes")), + u64::from_le_bytes(want[33..41].try_into().expect("8 bytes")) + )); + } + if got[41..43] != want[41..43] { + parts.push(format!( + "schema_id {:#06x} != {:#06x}", + u16::from_le_bytes(got[41..43].try_into().expect("2 bytes")), + u16::from_le_bytes(want[41..43].try_into().expect("2 bytes")) + )); + } + parts.join(", ") +} From fa675c7e4aff1f486d1ce743cfb6326c64d7ee73 Mon Sep 17 00:00:00 2001 From: Lucas Fiegl Date: Thu, 6 Aug 2026 10:33:40 -0300 Subject: [PATCH 27/30] ci: execute guest via ere-server twirp rpc --- .github/scripts/ere-execute.py | 124 +++++++++++++++++++++++++++++ .github/workflows/tag_release.yaml | 41 ++++++++-- 2 files changed, 160 insertions(+), 5 deletions(-) create mode 100755 .github/scripts/ere-execute.py diff --git a/.github/scripts/ere-execute.py b/.github/scripts/ere-execute.py new file mode 100755 index 00000000000..c852bedbf18 --- /dev/null +++ b/.github/scripts/ere-execute.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +"""Execute an ELF on a running `ere-server` and write its public values. + +`ere-server` has no `execute` subcommand — execution is a Twirp RPC +(`/twirp/api.ZkvmService/Execute`) served while the process runs in server mode. +See `crates/server/api/proto/api.proto` in eth-act/ere: + + message ExecuteRequest { bytes input_stdin = 1; optional bytes input_proofs = 2; } + message ExecuteResponse { oneof result { ExecuteOk ok = 1; string err = 2; } } + message ExecuteOk { bytes public_values = 1; bytes report = 2; } + +Speaks protobuf rather than Twirp's JSON on purpose. The generated types carry a +bare `#[derive(serde::Serialize)]` with no `rename_all` and no base64 helper, so +JSON would encode `bytes` as an array of integers — for a multi-megabyte witness +that is both enormous and needless. The two messages here are small enough to +encode and decode by hand, which also removes any guesswork about field naming. + +Usage: ere-execute.py +""" + +import sys +import urllib.error +import urllib.request + + +def encode_varint(value: int) -> bytes: + out = bytearray() + while True: + byte = value & 0x7F + value >>= 7 + out.append(byte | (0x80 if value else 0)) + if not value: + return bytes(out) + + +def decode_varint(buf: bytes, pos: int) -> tuple[int, int]: + value = shift = 0 + while True: + if pos >= len(buf): + raise ValueError("truncated varint") + byte = buf[pos] + pos += 1 + value |= (byte & 0x7F) << shift + if not byte & 0x80: + return value, pos + shift += 7 + if shift > 63: + raise ValueError("varint too long") + + +def fields(buf: bytes): + """Yield (field_number, wire_type, payload) for a protobuf message.""" + pos = 0 + while pos < len(buf): + key, pos = decode_varint(buf, pos) + field, wire = key >> 3, key & 0x07 + if wire == 2: # length-delimited + length, pos = decode_varint(buf, pos) + yield field, wire, buf[pos : pos + length] + pos += length + elif wire == 0: # varint + value, pos = decode_varint(buf, pos) + yield field, wire, value + elif wire == 5: + yield field, wire, buf[pos : pos + 4] + pos += 4 + elif wire == 1: + yield field, wire, buf[pos : pos + 8] + pos += 8 + else: + raise ValueError(f"unsupported wire type {wire} for field {field}") + + +def encode_execute_request(stdin: bytes) -> bytes: + """ExecuteRequest with only `input_stdin` (field 1, length-delimited).""" + return b"\x0a" + encode_varint(len(stdin)) + stdin + + +def decode_execute_response(body: bytes) -> bytes: + """Return `ok.public_values`, or raise with the server's `err` string.""" + for field, _wire, payload in fields(body): + if field == 1: # ExecuteOk + for inner_field, _w, inner in fields(payload): + if inner_field == 1: # public_values + return inner + raise ValueError("ExecuteOk carried no public_values") + if field == 2: # err + raise RuntimeError(f"guest execution failed: {payload.decode('utf-8', 'replace')}") + raise ValueError("ExecuteResponse set neither ok nor err") + + +def main() -> int: + if len(sys.argv) != 4: + print(__doc__, file=sys.stderr) + return 2 + url, input_path, output_path = sys.argv[1], sys.argv[2], sys.argv[3] + + with open(input_path, "rb") as handle: + stdin = handle.read() + print(f"executing with {len(stdin)} bytes of statelessInputBytes") + + request = urllib.request.Request( + url, + data=encode_execute_request(stdin), + headers={"Content-Type": "application/protobuf"}, + method="POST", + ) + try: + with urllib.request.urlopen(request, timeout=1800) as response: + body = response.read() + except urllib.error.HTTPError as err: + detail = err.read().decode("utf-8", "replace") + print(f"ere-server returned HTTP {err.code}: {detail}", file=sys.stderr) + return 1 + + public_values = decode_execute_response(body) + with open(output_path, "wb") as handle: + handle.write(public_values) + print(f"wrote {len(public_values)} bytes of statelessOutputBytes to {output_path}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/tag_release.yaml b/.github/workflows/tag_release.yaml index ca84610fe20..80a5918fce6 100644 --- a/.github/workflows/tag_release.yaml +++ b/.github/workflows/tag_release.yaml @@ -267,17 +267,48 @@ jobs: - name: Extract a true-success fixture run: .github/scripts/extract-stateless-fixture.sh - - name: Execute ELF under ere-server + # `ere-server` has no `execute` subcommand: its only commands are the + # server itself and `keygen`. Execution is the Twirp RPC + # `/twirp/api.ZkvmService/Execute`, served while it runs in server mode. + - name: Start ere-server run: | docker pull ghcr.io/eth-act/ere/ere-server-${{ matrix.zkvm }}:${ERE_TAG} - docker run \ + docker run -d --name ere-server \ -e RUST_LOG=info \ + -p 3000:3000 \ -v $PWD/output:/output \ ghcr.io/eth-act/ere/ere-server-${{ matrix.zkvm }}:${ERE_TAG} \ + --port 3000 \ --elf-path /output/${{ steps.artifact.outputs.name }}.elf \ - execute \ - --input-path /output/stateless-input.bin \ - --output-path /output/stateless-output.bin + cpu + + # `/health` flips to 200 once the ELF is loaded and the server is up. + for _ in $(seq 1 60); do + if curl -sf http://localhost:3000/health > /dev/null; then + echo "ere-server is up" + exit 0 + fi + if [ -z "$(docker ps -q -f name=ere-server)" ]; then + echo "::error::ere-server exited before becoming healthy" + docker logs ere-server + exit 1 + fi + sleep 2 + done + echo "::error::ere-server did not become healthy within 120s" + docker logs ere-server + exit 1 + + - name: Execute ELF under ere-server + run: | + .github/scripts/ere-execute.py \ + http://localhost:3000/twirp/api.ZkvmService/Execute \ + output/stateless-input.bin \ + output/stateless-output.bin + + - name: ere-server logs + if: always() + run: docker logs ere-server 2>&1 | tail -50 || true - name: Compare against the fixture run: | From 5c3af7e046552187301b5cfab734031fcb2ed90e Mon Sep 17 00:00:00 2001 From: Lucas Fiegl Date: Thu, 6 Aug 2026 10:56:45 -0300 Subject: [PATCH 28/30] ci: allow zisk output padding, widen openvm startup --- .github/workflows/tag_release.yaml | 62 +++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/.github/workflows/tag_release.yaml b/.github/workflows/tag_release.yaml index 80a5918fce6..f4cc8574078 100644 --- a/.github/workflows/tag_release.yaml +++ b/.github/workflows/tag_release.yaml @@ -282,10 +282,15 @@ jobs: --elf-path /output/${{ steps.artifact.outputs.name }}.elf \ cpu - # `/health` flips to 200 once the ELF is loaded and the server is up. - for _ in $(seq 1 60); do + # `/health` flips to 200 once the server is ready. Allow a long window: + # openvm runs a full STARK keygen on startup before it serves, which is + # minutes of work, and it does so even though we only ever execute. The + # container-exit check below means a genuine crash still fails fast + # instead of burning the whole window. + deadline=$((SECONDS + 900)) + while [ "$SECONDS" -lt "$deadline" ]; do if curl -sf http://localhost:3000/health > /dev/null; then - echo "ere-server is up" + echo "ere-server is up after ${SECONDS}s" exit 0 fi if [ -z "$(docker ps -q -f name=ere-server)" ]; then @@ -293,10 +298,13 @@ jobs: docker logs ere-server exit 1 fi - sleep 2 + if [ $((SECONDS % 60)) -lt 3 ]; then + echo " still waiting for ere-server (${SECONDS}s elapsed)" + fi + sleep 3 done - echo "::error::ere-server did not become healthy within 120s" - docker logs ere-server + echo "::error::ere-server did not become healthy within 900s" + docker logs ere-server 2>&1 | tail -50 exit 1 - name: Execute ELF under ere-server @@ -312,13 +320,49 @@ jobs: - name: Compare against the fixture run: | - if ! cmp -s output/stateless-output.bin output/expected-output.bin; then - echo "statelessOutputBytes mismatch" + set -euo pipefail + expected_len=$(wc -c < output/expected-output.bin | tr -d ' ') + actual_len=$(wc -c < output/stateless-output.bin | tr -d ' ') + + # The committed public values must BEGIN with the spec's + # statelessOutputBytes, and anything after them must be zero. + # + # Not a loosening for its own sake: ZisK enforces a fixed 256-byte + # output at the runtime level (see ere's zisk platform.rs), so its + # 43 meaningful bytes always arrive zero-padded. Requiring an exact + # length would fail ZisK on every correct run. The rule still pins + # every meaningful byte, so it cannot accept a wrong value — only + # trailing zeroes the runtime added. + if [ "$actual_len" -lt "$expected_len" ]; then + echo "::error::output is $actual_len bytes, shorter than the expected $expected_len" + echo "expected: $(xxd -p output/expected-output.bin | tr -d '\n')" + echo "actual: $(xxd -p output/stateless-output.bin | tr -d '\n')" + exit 1 + fi + + # `head -c | cmp -` rather than `cmp -n`: BSD cmp reports EOF on the + # shorter file as a difference even inside the byte limit, so `-n` + # would fail every padded ZisK run. This form is unambiguous on both + # BSD and GNU. + if ! head -c "$expected_len" output/stateless-output.bin \ + | cmp -s - output/expected-output.bin; then + echo "::error::statelessOutputBytes mismatch in the first $expected_len bytes" echo "expected: $(xxd -p output/expected-output.bin | tr -d '\n')" echo "actual: $(xxd -p output/stateless-output.bin | tr -d '\n')" exit 1 fi - echo "statelessOutputBytes match" + + if [ "$actual_len" -gt "$expected_len" ]; then + nonzero=$(tail -c "+$((expected_len + 1))" output/stateless-output.bin | tr -d '\000' | wc -c | tr -d ' ') + if [ "$nonzero" -ne 0 ]; then + echo "::error::$nonzero non-zero byte(s) past the expected $expected_len" + echo "actual: $(xxd -p output/stateless-output.bin | tr -d '\n')" + exit 1 + fi + echo "statelessOutputBytes match ($expected_len bytes, plus $((actual_len - expected_len)) bytes of runtime zero padding)" + else + echo "statelessOutputBytes match ($expected_len bytes, exact)" + fi package-contracts: needs: From ef7f3758358483eee37d0064e68030c5ffe5c138 Mon Sep 17 00:00:00 2001 From: Lucas Fiegl Date: Thu, 6 Aug 2026 11:21:08 -0300 Subject: [PATCH 29/30] fix(ci): write VK into the mounted output dir --- .github/workflows/tag_release.yaml | 43 +++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tag_release.yaml b/.github/workflows/tag_release.yaml index f4cc8574078..203e2988f6e 100644 --- a/.github/workflows/tag_release.yaml +++ b/.github/workflows/tag_release.yaml @@ -207,7 +207,32 @@ jobs: ghcr.io/eth-act/ere/ere-server-${{ matrix.zkvm }}:${ERE_TAG} \ --elf-path /output/${{ steps.artifact.outputs.name }}.elf \ keygen \ - --program-vk-path ${{ steps.artifact.outputs.name }}.vk + --program-vk-path /output/${{ steps.artifact.outputs.name }}.vk + + # `if-no-files-found: error` only fires when NOTHING matches, so a + # multi-line `path:` where one pattern matches and another does not + # uploads a subset and still reports success. That is how the verification + # keys went missing: `--program-vk-path` was relative, so ere-server wrote + # each .vk inside the container and it died with it, while the .elf still + # matched and the upload went green. Assert both files explicitly. + - name: Assert the ELF and VK both exist + run: | + set -euo pipefail + missing=0 + for ext in elf vk; do + file="output/${{ steps.artifact.outputs.name }}.$ext" + if [ ! -s "$file" ]; then + echo "::error::$file is missing or empty" + missing=1 + else + echo " $file $(wc -c < "$file" | tr -d ' ') bytes" + fi + done + if [ "$missing" -ne 0 ]; then + echo "--- contents of output/ ---" + ls -la output/ + exit 1 + fi - name: Upload artifact uses: actions/upload-artifact@v6 @@ -565,8 +590,20 @@ jobs: count=$((count + 1)) done < <(find ./bin -type f -name 'stateless-validator-ethrex-*' \ \( -name '*.elf' -o -name '*.vk' \) -print0) - if [ "$count" -eq 0 ]; then - echo "::error::no stateless-validator artifacts were verified" + # Both kinds must be present: the handbook requires the ELF *and* the + # verification key to ship signed, and an ELF-only release is exactly + # what a silently-partial upload produces. + elfs=$(find ./bin -type f -name 'stateless-validator-ethrex-*.elf' | wc -l | tr -d ' ') + vks=$(find ./bin -type f -name 'stateless-validator-ethrex-*.vk' | wc -l | tr -d ' ') + echo "signed $elfs ELF(s) and $vks verification key(s)" + if [ "$count" -eq 0 ] || [ "$elfs" -eq 0 ] || [ "$vks" -eq 0 ]; then + echo "::error::expected both ELFs and verification keys; got $elfs ELF(s), $vks VK(s)" + find ./bin -type f | sort + exit 1 + fi + if [ "$elfs" -ne "$vks" ]; then + echo "::error::$elfs ELF(s) but $vks verification key(s) — every ELF needs its VK" + find ./bin -type f | sort exit 1 fi echo "Verified $count signature(s) against the committed public key." From 499826060f38435addb5dd19d7bf6afb263dcc5e Mon Sep 17 00:00:00 2001 From: Lucas Fiegl Date: Thu, 6 Aug 2026 12:17:57 -0300 Subject: [PATCH 30/30] fix(ci): v-prefix zkvm version in asset names --- .github/scripts/zkvm-version.sh | 12 +++++++++--- .github/workflows/pr_lint_gha.yaml | 4 ++-- docs/eip-8025.md | 15 ++++++++++----- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/.github/scripts/zkvm-version.sh b/.github/scripts/zkvm-version.sh index 9cf4efd2756..1fd525c2a30 100755 --- a/.github/scripts/zkvm-version.sh +++ b/.github/scripts/zkvm-version.sh @@ -3,6 +3,12 @@ # Prints the zkVM SDK version the stateless-validator guest is built against, for # use in release asset names. # +# The `v` prefix is part of the name, not decoration: eth-act/ere-guests +# publishes `stateless-validator---v.{elf,vk}` (see its +# v0.15.0 assets, and the example commands in ere's docs/vk-generation.md), and +# it republishes guest ELFs verbatim. Dropping the `v` makes our assets not +# drop-in for that pipeline. +# # The guests reach their SDK through `ere-platform-{zisk,sp1,openvm}`, so there is # no direct `tag = "vX.Y.Z"` in their manifests to read. The versions are instead # fixed by the pinned `ere` revision below, mirroring what `ere-catalog` resolves @@ -19,9 +25,9 @@ ERE_REV=a25f1aed9664c3b63e73ef05360090a4c41da31b # SDK versions resolved by ere-catalog at ERE_REV. zkvm_version() { case "$1" in - zisk) echo "1.0.0-alpha" ;; - sp1) echo "6.3.1" ;; - openvm) echo "2.0.0" ;; + zisk) echo "v1.0.0-alpha" ;; + sp1) echo "v6.3.1" ;; + openvm) echo "v2.0.0" ;; *) echo "unknown zkvm: $1" >&2; return 1 ;; esac } diff --git a/.github/workflows/pr_lint_gha.yaml b/.github/workflows/pr_lint_gha.yaml index 8794fa1d79d..0694e824f77 100644 --- a/.github/workflows/pr_lint_gha.yaml +++ b/.github/workflows/pr_lint_gha.yaml @@ -124,7 +124,7 @@ jobs: make_tree() { local root="$1" rm -rf "$root" - for name in stateless-validator-ethrex-sp1-6.3.1 stateless-validator-ethrex-zisk-1.0.0-alpha; do + for name in stateless-validator-ethrex-sp1-v6.3.1 stateless-validator-ethrex-zisk-v1.0.0-alpha; do mkdir -p "$root/$name" head -c 256 /dev/urandom > "$root/$name/$name.elf" head -c 64 /dev/urandom > "$root/$name/$name.vk" @@ -195,7 +195,7 @@ jobs: # A tampered artifact must stop verifying, proving the signature binds # the bytes rather than just existing. - elf="$work/bin2/stateless-validator-ethrex-sp1-6.3.1/stateless-validator-ethrex-sp1-6.3.1.elf" + elf="$work/bin2/stateless-validator-ethrex-sp1-v6.3.1/stateless-validator-ethrex-sp1-v6.3.1.elf" printf 'x' >> "$elf" if minisign -V -m "$elf" -p "$work/pub" > /dev/null 2>&1; then echo "::error::a tampered artifact still verified" diff --git a/docs/eip-8025.md b/docs/eip-8025.md index 9c9ce8b5d79..c2a628068f1 100644 --- a/docs/eip-8025.md +++ b/docs/eip-8025.md @@ -183,12 +183,17 @@ to keep in step with the SDK. `tag_release.yaml` builds, keygens, verifies and attaches, per zkVM: ``` -stateless-validator-ethrex-zisk-1.0.0-alpha.elf + .vk (+ .minisig each) -stateless-validator-ethrex-sp1-6.3.1.elf + .vk (+ .minisig each) -stateless-validator-ethrex-openvm-2.0.0.elf + .vk (+ .minisig each) +stateless-validator-ethrex-zisk-v1.0.0-alpha.elf + .vk (+ .minisig each) +stateless-validator-ethrex-sp1-v6.3.1.elf + .vk (+ .minisig each) +stateless-validator-ethrex-openvm-v2.0.0.elf + .vk (+ .minisig each) minisign.pub ``` +The `v` on the zkVM version is required, not stylistic: `eth-act/ere-guests` +publishes `stateless-validator---v.{elf,vk}` and +republishes guest ELFs verbatim, so an asset without it is not drop-in for that +pipeline. `.github/scripts/zkvm-version.sh` emits the prefix. + Compilation runs in `ghcr.io/eth-act/ere/ere-compiler-`, and `.vk` comes from `ere-server … keygen`, so the artifacts are produced by the same toolchain that will consume them. Version strings come from `.github/scripts/zkvm-version.sh`, which @@ -211,7 +216,7 @@ minisign.pub Verify a downloaded asset with: ```bash -minisign -Vm stateless-validator-ethrex-sp1-6.3.1.elf -p minisign.pub +minisign -Vm stateless-validator-ethrex-sp1-v6.3.1.elf -p minisign.pub ``` The trusted comment — which is covered by the signature, unlike the untrusted @@ -219,7 +224,7 @@ one — carries the asset name, tag and commit, so a verified signature also attests which commit produced the artifact: ``` -Trusted comment: stateless-validator-ethrex-sp1-6.3.1.elf ethrex v9.0.0-rc1 3f2a1c… +Trusted comment: stateless-validator-ethrex-sp1-v6.3.1.elf ethrex v9.0.0-rc1 3f2a1c… ``` **Prefer `.github/minisign.pub` from this repository over the copy in the