From 89d3141781c5ba65c449baa6de4244e9a680ca1a Mon Sep 17 00:00:00 2001 From: frisitano Date: Sat, 21 Mar 2026 02:11:26 +0000 Subject: [PATCH 1/4] integrate zkboost --- .../kurtosis_zkboost/kurtosis.yml | 1 + .../local_testnet/kurtosis_zkboost/main.star | 144 ++++++++++++++++++ .../network_params_eip8025_zkboost.yaml | 64 ++++++++ .../start_eip8025_zkboost_testnet.sh | 84 ++++++++++ 4 files changed, 293 insertions(+) create mode 100644 scripts/local_testnet/kurtosis_zkboost/kurtosis.yml create mode 100644 scripts/local_testnet/kurtosis_zkboost/main.star create mode 100644 scripts/local_testnet/network_params_eip8025_zkboost.yaml create mode 100755 scripts/local_testnet/start_eip8025_zkboost_testnet.sh diff --git a/scripts/local_testnet/kurtosis_zkboost/kurtosis.yml b/scripts/local_testnet/kurtosis_zkboost/kurtosis.yml new file mode 100644 index 00000000000..eef19f6c086 --- /dev/null +++ b/scripts/local_testnet/kurtosis_zkboost/kurtosis.yml @@ -0,0 +1 @@ +name: github.com/sigp/lighthouse/scripts/local_testnet/kurtosis_zkboost diff --git a/scripts/local_testnet/kurtosis_zkboost/main.star b/scripts/local_testnet/kurtosis_zkboost/main.star new file mode 100644 index 00000000000..31ecc1949c6 --- /dev/null +++ b/scripts/local_testnet/kurtosis_zkboost/main.star @@ -0,0 +1,144 @@ +# Kurtosis package that runs the ethereum-package and then adds zkboost-server +# sidecar services for real proof generation. +# +# Usage: +# kurtosis run --enclave eip8025-zkboost ./kurtosis_zkboost \ +# --args-file network_params_eip8025_zkboost.yaml +# +# The args file must include a top-level `zkboost` key alongside standard +# ethereum-package configuration. Example: +# +# zkboost: +# image: ghcr.io/eth-act/zkboost/zkboost-server:1715344 +# instances: +# - name: zkboost-1 +# el_service: el-1-geth-lighthouse +# - name: zkboost-2 +# el_service: el-2-geth-lighthouse +# mock_proving_time_ms: 5000 +# mock_proof_size: 1024 + +ethereum_package = import_module("github.com/ethpandaops/ethereum-package/main.star") + +ZKBOOST_PORT_ID = "http" +ZKBOOST_PORT_NUMBER = 3000 +ZKBOOST_METRICS_PATH = "/metrics" + +# Default mock zkVM config — real proving backends can be configured via +# external ere-server instances if needed. +ZKBOOST_CONFIG_TEMPLATE = """\ +port = {port} +el_endpoint = "http://{el_service}:{el_rpc_port}" +chain_config_path = "/app/chain_config.json" + +[[zkvm]] +kind = "mock" +mock_proving_time_ms = {mock_proving_time_ms} +mock_proof_size = {mock_proof_size} +proof_type = "reth-zisk" +""" + +# Chain config matching the Kurtosis devnet genesis (chainId 3151908). +# Providing this as a file avoids the debug_chainConfig RPC call, which +# is not available in standard geth. +CHAIN_CONFIG_JSON = """\ +{ + "chainId": 3151908, + "homesteadBlock": 0, + "daoForkSupport": false, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "berlinBlock": 0, + "londonBlock": 0, + "mergeNetsplitBlock": 0, + "shanghaiTime": 0, + "cancunTime": 0, + "pragueTime": 0, + "osakaTime": 0, + "bpo1Time": 0, + "terminalTotalDifficulty": 0, + "terminalTotalDifficultyPassed": true, + "depositContractAddress": "0x00000000219ab540356cbb839cbe05303d7705fa", + "blobSchedule": { + "bpo1": { "baseFeeUpdateFraction": 8346193, "max": 15, "target": 10 }, + "cancun": { "baseFeeUpdateFraction": 3338477, "max": 6, "target": 3 }, + "osaka": { "baseFeeUpdateFraction": 5007716, "max": 9, "target": 6 }, + "prague": { "baseFeeUpdateFraction": 5007716, "max": 9, "target": 6 } + } +} +""" + +def run(plan, args): + """Start ethereum-package then add zkboost-server sidecars.""" + + # Split out zkboost config from ethereum-package args. + zkboost_args = args.pop("zkboost", None) + if zkboost_args == None: + fail("Missing required 'zkboost' key in args file.") + + # Run the standard ethereum-package with the remaining args. + ethereum_package.run(plan, args) + + # Extract zkboost settings with defaults. + zkboost_image = zkboost_args.get("image", "ghcr.io/eth-act/zkboost/zkboost-server:1715344") + instances = zkboost_args.get("instances", []) + mock_proving_time_ms = zkboost_args.get("mock_proving_time_ms", 5000) + mock_proof_size = zkboost_args.get("mock_proof_size", 1024) + el_rpc_port = zkboost_args.get("el_rpc_port", 8545) + + if len(instances) == 0: + fail("zkboost.instances must contain at least one entry.") + + for instance in instances: + name = instance["name"] + el_service = instance["el_service"] + + config_content = ZKBOOST_CONFIG_TEMPLATE.format( + port = ZKBOOST_PORT_NUMBER, + el_service = el_service, + el_rpc_port = el_rpc_port, + mock_proving_time_ms = mock_proving_time_ms, + mock_proof_size = mock_proof_size, + ) + + config_artifact = plan.render_templates( + name = name + "-config", + config = { + "config.toml": struct( + template = config_content, + data = {}, + ), + "chain_config.json": struct( + template = CHAIN_CONFIG_JSON, + data = {}, + ), + }, + ) + + plan.add_service( + name = name, + config = ServiceConfig( + image = zkboost_image, + cmd = ["--config", "/app/config.toml"], + ports = { + ZKBOOST_PORT_ID: PortSpec( + number = ZKBOOST_PORT_NUMBER, + transport_protocol = "TCP", + application_protocol = "http", + ), + }, + files = { + "/app": config_artifact, + }, + env_vars = { + "RUST_LOG": "info,zkboost=debug", + }, + ), + ) + + plan.print("Started zkboost service '{0}' -> EL '{1}'".format(name, el_service)) diff --git a/scripts/local_testnet/network_params_eip8025_zkboost.yaml b/scripts/local_testnet/network_params_eip8025_zkboost.yaml new file mode 100644 index 00000000000..7c5e677222c --- /dev/null +++ b/scripts/local_testnet/network_params_eip8025_zkboost.yaml @@ -0,0 +1,64 @@ +# EIP-8025 testnet with real zkboost-server backends. +# +# This config is consumed by the kurtosis_zkboost wrapper package, which starts +# the ethereum-package first and then adds zkboost-server sidecar services. +# +# The `zkboost` section is stripped from args before forwarding to +# ethereum-package. CL/VC nodes point --proof-engine-endpoint at the zkboost +# service running inside the same Kurtosis enclave. +# +# For the mock-only path, use network_params_eip8025.yaml instead. + +# ── Ethereum package participants ──────────────────────────────────────────── +participants: + # Supernode participants — proof engine endpoint points to zkboost-1 + - cl_type: lighthouse + cl_image: lighthouse:local + el_type: reth + el_image: ghcr.io/paradigmxyz/reth + supernode: true + cl_extra_params: + - --target-peers=3 + - --proof-engine-endpoint=http://zkboost-1:3000 + vc_extra_params: + - --proof-engine-endpoint=http://zkboost-1:3000 + - --proof-types=6 + count: 2 + # Non-supernode participants — proof engine endpoint points to zkboost-2 + - cl_type: lighthouse + cl_image: lighthouse:local + el_type: reth + el_image: ghcr.io/paradigmxyz/reth + supernode: false + cl_extra_params: + - --target-peers=3 + - --proof-engine-endpoint=http://zkboost-2:3000 + vc_extra_params: + - --proof-engine-endpoint=http://zkboost-2:3000 + - --proof-types=6 + count: 2 + +network_params: + fulu_fork_epoch: 0 + seconds_per_slot: 6 + +snooper_enabled: false +global_log_level: debug + +additional_services: + - dora + - prometheus_grafana + +# ── zkboost-server sidecar configuration ───────────────────────────────────── +# Processed by kurtosis_zkboost/main.star; NOT forwarded to ethereum-package. +zkboost: + image: zkboost-server:local + # Each instance connects to one EL node's JSON-RPC endpoint for witness data. + instances: + - name: zkboost-1 + el_service: el-1-reth-lighthouse + - name: zkboost-2 + el_service: el-2-reth-lighthouse + # Mock zkVM settings (real zkVM backends need external ere-server instances). + mock_proving_time_ms: 5000 + mock_proof_size: 1024 diff --git a/scripts/local_testnet/start_eip8025_zkboost_testnet.sh b/scripts/local_testnet/start_eip8025_zkboost_testnet.sh new file mode 100755 index 00000000000..e3bd3d304a4 --- /dev/null +++ b/scripts/local_testnet/start_eip8025_zkboost_testnet.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash + +# Start a local EIP-8025 testnet with real zkboost-server backends using Kurtosis. +# +# This script builds Lighthouse and launches a Kurtosis enclave using the +# kurtosis_zkboost wrapper package, which first starts the ethereum-package +# and then adds zkboost-server sidecar services. +# +# For the mock-only path, use start_eip8025_testnet.sh instead. +# +# Requires: docker, kurtosis, yq + +set -Eeuo pipefail + +SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +ROOT_DIR="$SCRIPT_DIR/../.." +ENCLAVE_NAME=eip8025-zkboost +NETWORK_PARAMS_FILE=$SCRIPT_DIR/network_params_eip8025_zkboost.yaml +KURTOSIS_PKG_DIR=$SCRIPT_DIR/kurtosis_zkboost + +BUILD_IMAGE=true +KEEP_ENCLAVE=false + +# Get options +while getopts "e:n:bkh" flag; do + case "${flag}" in + e) ENCLAVE_NAME=${OPTARG};; + n) NETWORK_PARAMS_FILE=${OPTARG};; + b) BUILD_IMAGE=false;; + k) KEEP_ENCLAVE=true;; + h) + echo "Start a local EIP-8025 testnet with real zkboost backends." + echo + echo "usage: $0 " + echo + echo "Options:" + echo " -e: enclave name default: $ENCLAVE_NAME" + echo " -n: kurtosis network params file path default: $NETWORK_PARAMS_FILE" + echo " -b: skip building Lighthouse docker image" + echo " -k: keep existing enclave (don't destroy first)" + echo " -h: this help" + exit + ;; + esac +done + +LH_IMAGE_NAME=$(yq eval ".participants[0].cl_image" "$NETWORK_PARAMS_FILE") + +for cmd in docker kurtosis yq; do + if ! command -v "$cmd" &> /dev/null; then + echo "$cmd is not installed. Please install $cmd and try again." + exit 1 + fi +done + +if [ "$KEEP_ENCLAVE" = false ]; then + kurtosis enclave rm -f "$ENCLAVE_NAME" 2>/dev/null || true +fi + +if [ "$BUILD_IMAGE" = true ]; then + echo "Building Lighthouse Docker image." + docker build \ + --build-arg FEATURES=portable,spec-minimal \ + -f "$ROOT_DIR/Dockerfile" \ + -t "$LH_IMAGE_NAME" \ + "$ROOT_DIR" +else + echo "Skipping Lighthouse Docker image build." +fi + +echo "Starting EIP-8025 zkboost testnet enclave: $ENCLAVE_NAME" +kurtosis run --enclave "$ENCLAVE_NAME" \ + "$KURTOSIS_PKG_DIR" \ + --args-file "$NETWORK_PARAMS_FILE" + +echo "" +echo "EIP-8025 zkboost testnet started!" +echo "" +echo "Useful commands:" +echo " kurtosis enclave inspect $ENCLAVE_NAME" +echo " kurtosis service logs $ENCLAVE_NAME cl-1-lighthouse-reth" +echo " kurtosis service logs $ENCLAVE_NAME zkboost-1" +echo " kurtosis service logs $ENCLAVE_NAME zkboost-2" +echo " kurtosis enclave rm -f $ENCLAVE_NAME" From e30220500e9983dc6ee610b4da3089a1bb353023 Mon Sep 17 00:00:00 2001 From: frisitano Date: Wed, 25 Mar 2026 04:43:37 +0100 Subject: [PATCH 2/4] improvements to sync protocol --- Cargo.lock | 2 + beacon_node/beacon_chain/src/beacon_chain.rs | 15 ++-- .../beacon_chain/src/bellatrix_readiness.rs | 12 +-- beacon_node/beacon_chain/src/builder.rs | 39 +++++++-- .../beacon_chain/src/execution_payload.rs | 2 +- .../src/eip8025/proof_engine.rs | 2 +- .../execution_layer/src/eip8025/state.rs | 81 +++++++++++++++---- beacon_node/execution_layer/src/engine_api.rs | 4 +- beacon_node/execution_layer/src/engines.rs | 11 +++ beacon_node/execution_layer/src/lib.rs | 2 +- .../src/test_utils/mock_proof_node_client.rs | 2 +- .../execution_layer/src/test_utils/mod.rs | 4 +- beacon_node/network/src/sync/proof_sync.rs | 9 +++ beacon_node/network/src/sync/tests/range.rs | 1 + beacon_node/store/src/hot_cold_store.rs | 13 +-- .../local_testnet/kurtosis_zkboost/main.star | 39 --------- .../network_params_eip8025_zkboost.yaml | 4 +- testing/proof_engine_zkboost/Cargo.toml | 2 + testing/proof_engine_zkboost/src/lib.rs | 17 +++- 19 files changed, 159 insertions(+), 102 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6f8fceefdd5..b496c4b9dbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9029,6 +9029,7 @@ dependencies = [ "anyhow", "axum 0.7.9", "bytes", + "ethereum_ssz 0.10.1", "execution_layer", "futures", "metrics-exporter-prometheus", @@ -9041,6 +9042,7 @@ dependencies = [ "tokio-stream", "tokio-util", "tracing", + "tree_hash 0.12.1", "types 0.2.1", "url", "zkboost-server", diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 50d99a60092..9869c259b1e 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -7482,7 +7482,9 @@ impl BeaconChain { pe.missing_proofs() .into_iter() .filter_map(|mut info| { - info.root = self.store.get_block_root_by_request_root(&info.root)?; + let (block_root, slot) = self.store.get_block_root_by_request_root(&info.root)?; + info.root = block_root; + info.slot = slot; Some(info) }) .collect() @@ -7604,7 +7606,7 @@ impl BeaconChain { let request_root = signed_proof.request_root(); // Look up the beacon block root from request root - let block_root = self + let (block_root, slot) = self .store .get_block_root_by_request_root(&request_root) .ok_or_else(|| ExecutionProofError::UnknownRequestRoot(request_root))?; @@ -7654,14 +7656,7 @@ impl BeaconChain { Err(e) => return Err(Error::ForkChoiceError(e)), } - // Look up the slot so caller can update local execution proof status. - let slot = self - .store - .get_blinded_block(&block_root) - .ok() - .flatten() - .map(|b| b.slot()); - return Ok((verification_result, slot.map(|s| (block_root, s)))); + return Ok((verification_result, Some((block_root, slot)))); } Ok((verification_result, None)) diff --git a/beacon_node/beacon_chain/src/bellatrix_readiness.rs b/beacon_node/beacon_chain/src/bellatrix_readiness.rs index d588885ea1d..6e702ce2856 100644 --- a/beacon_node/beacon_chain/src/bellatrix_readiness.rs +++ b/beacon_node/beacon_chain/src/bellatrix_readiness.rs @@ -2,7 +2,7 @@ //! transition. use crate::{BeaconChain, BeaconChainError as Error, BeaconChainTypes}; -use execution_layer::{BlockByNumberQuery, ForkchoiceState}; +use execution_layer::BlockByNumberQuery; use serde::{Deserialize, Serialize, Serializer}; use std::fmt; use std::fmt::Write; @@ -205,16 +205,6 @@ impl BeaconChain { .ok_or(Error::ExecutionLayerMissing)?; let exec_block_hash = latest_execution_payload_header.block_hash(); - if let Some(proof_engine) = execution_layer.proof_engine() { - proof_engine - .forkchoice_updated(ForkchoiceState { - head_block_hash: exec_block_hash, - safe_block_hash: exec_block_hash, - finalized_block_hash: exec_block_hash, - }) - .await?; - } - // Use getBlockByNumber(0) to check that the block hash matches. // At present, Geth does not respond to engine_getPayloadBodiesByRange before genesis. if execution_layer.engine().is_some() { diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index e8f0a8962c6..f9654bb7668 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -23,7 +23,7 @@ use crate::{ BeaconChain, BeaconChainTypes, BeaconForkChoiceStore, BeaconSnapshot, ServerSentEventHandler, }; use bls::Signature; -use execution_layer::ExecutionLayer; +use execution_layer::{ExecutionLayer, ForkchoiceState}; use fixed_bytes::FixedBytesExtended; use fork_choice::{ForkChoice, ResetPayloadStatuses}; use futures::channel::mpsc::Sender; @@ -42,7 +42,7 @@ use std::sync::Arc; use std::time::Duration; use store::{Error as StoreError, HotColdDB, ItemStore, KeyValueStoreOp}; use task_executor::{ShutdownReason, TaskExecutor}; -use tracing::{debug, error, info}; +use tracing::{debug, error, info, warn}; use types::data::CustodyIndex; use types::{ BeaconBlock, BeaconState, BlobSidecarList, ChainSpec, ColumnIndex, DataColumnSidecarList, @@ -917,6 +917,15 @@ where let genesis_validators_root = head_snapshot.beacon_state.genesis_validators_root(); let genesis_time = head_snapshot.beacon_state.genesis_time(); + let genesis_execution_block_hash = (head_snapshot.beacon_state.slot() == 0) + .then(|| { + head_snapshot + .beacon_state + .latest_execution_payload_header() + .ok() + .map(|header| header.block_hash()) + }) + .flatten(); let canonical_head = CanonicalHead::new(fork_choice, Arc::new(head_snapshot)); let shuffling_cache_size = self.chain_config.shuffling_cache_size; let complete_blob_backfill = self.chain_config.complete_blob_backfill; @@ -976,18 +985,32 @@ where }; debug!(?custody_context, "Loaded persisted custody context"); - // Restore ProofEngine state from disk if available. + // Restore ProofEngine state from disk if available, or seed from genesis on fresh start. if let Some(proof_engine) = self .execution_layer .as_ref() .and_then(|el| el.proof_engine()) && let Some(store) = self.store - && let Some(persisted) = - crate::BeaconChain::>::load_proof_engine_state( - store.clone(), - ) { - proof_engine.restore_from_persisted(persisted); + match crate::BeaconChain::>::load_proof_engine_state( + store.clone(), + ) { + Some(persisted) => proof_engine.restore_from_persisted(persisted), + None if genesis_execution_block_hash.is_some() => { + proof_engine + .forkchoice_updated(ForkchoiceState::new_genesis( + genesis_execution_block_hash.expect("is Some"), + )) + .map_err(|err| { + format!("failed to seed proof engine with genesis hash: {err:?}") + })?; + } + _ => { + warn!( + "No persisted ProofEngine state and head is not at genesis. ProofEngine may be out of sync until next fork choice update." + ); + } + } } let beacon_chain = BeaconChain { diff --git a/beacon_node/beacon_chain/src/execution_payload.rs b/beacon_node/beacon_chain/src/execution_payload.rs index 61ad7714d05..a14c6e302f1 100644 --- a/beacon_node/beacon_chain/src/execution_payload.rs +++ b/beacon_node/beacon_chain/src/execution_payload.rs @@ -155,7 +155,7 @@ async fn notify_new_payload( ); chain .store - .put_request_root_mapping(new_payload_request_root, block_root); + .put_request_root_mapping(new_payload_request_root, block_root, block.slot()); match new_payload_response { Ok(status) => match status { diff --git a/beacon_node/execution_layer/src/eip8025/proof_engine.rs b/beacon_node/execution_layer/src/eip8025/proof_engine.rs index 9a63d0bb664..5ba67e948f1 100644 --- a/beacon_node/execution_layer/src/eip8025/proof_engine.rs +++ b/beacon_node/execution_layer/src/eip8025/proof_engine.rs @@ -165,7 +165,7 @@ impl HttpProofEngine { } /// Notify the proof engine of a forkchoice update. - pub async fn forkchoice_updated( + pub fn forkchoice_updated( &self, forkchoice_state: ForkchoiceState, ) -> Result { diff --git a/beacon_node/execution_layer/src/eip8025/state.rs b/beacon_node/execution_layer/src/eip8025/state.rs index 509a502f79a..4f090c17dd4 100644 --- a/beacon_node/execution_layer/src/eip8025/state.rs +++ b/beacon_node/execution_layer/src/eip8025/state.rs @@ -51,23 +51,46 @@ impl State { Self::default() } - /// Return all buffer entries that do not yet have sufficient proofs for promotion. + /// Return buffer entries that do not yet have sufficient proofs for promotion, + /// restricted to those on the ancestor path required to satisfy `latest_fcs`. /// - /// Only the `buffer` is scanned: by design, every entry in the buffer has not been - /// promoted to the tree, meaning it lacks sufficient proofs. Tree entries are already done. + /// If `latest_fcs` is unset there is no pending fork-choice update to satisfy, so + /// nothing is returned. Otherwise the buffer is walked backwards from + /// `latest_fcs.head_block_hash`; entries that lack sufficient proofs are collected + /// until a block is not found in the buffer (reached the tree or an unseen block). pub fn missing_proofs(&self) -> Vec { - self.buffer + let Some(latest_fcs) = &self.latest_fcs else { + return vec![]; + }; + + // Build block_hash → &PayloadRequest for O(1) lookup during the walk. + let buffer_by_block_hash: HashMap = self + .buffer .proofs - .iter() - .map(|(request_root, payload_request)| MissingProofInfo { - root: *request_root, - existing_proof_types: payload_request - .proofs - .iter() - .map(|p| p.message.proof_type) - .collect(), - }) - .collect() + .values() + .map(|p| (p.metadata.block_hash, p)) + .collect(); + + // Walk backwards from the FCS head through buffer entries, collecting + // those that still lack sufficient proofs. Stop when a block is not in + // the buffer (reached the tree or an unseen block). + let mut result = Vec::new(); + let mut current = latest_fcs.head_block_hash; + loop { + let Some(req) = buffer_by_block_hash.get(¤t) else { + break; + }; + if req.proofs.len() < self.min_required_proofs { + result.push(MissingProofInfo { + root: req.metadata.request_root, + existing_proof_types: req.proofs.iter().map(|p| p.message.proof_type).collect(), + slot: Default::default(), // populated by BeaconChain::missing_execution_proofs() + }); + } + current = req.metadata.parent_hash; + } + + result } /// Check if the state contains any proofs associated with the given new payload request root. @@ -121,6 +144,21 @@ impl State { self.tree.current_canonical_head = finalized; tracing::info!(target: "execution_layer", ?finalized, "Updated last_valid_fcs to finalized block (tree empty)"); + + // Check if any buffered requests can be promoted based on the new last_valid_fcs. + let mut promote_requests = Vec::new(); + for request in self.buffer.proofs.keys() { + if self.can_promote(request)? { + promote_requests.push(*request); + } + } + // Promote any buffered requests that can now be associated with the tree state. + for request_root in promote_requests { + if let Some(latest_canonical_head) = self.promote_buffered_requests(request_root)? { + tracing::info!(target: "execution_layer", ?latest_canonical_head, "Updated canonical head after promoting buffered proofs"); + } + } + return Ok(self.forkchoice_response_syncing()); } @@ -653,11 +691,19 @@ pub mod test_utils { pub fn create_signed_proof( request_root: Hash256, validator_index: u64, + ) -> SignedExecutionProof { + create_signed_proof_with_type(request_root, validator_index, 1) + } + + pub fn create_signed_proof_with_type( + request_root: Hash256, + validator_index: u64, + proof_type: u8, ) -> SignedExecutionProof { SignedExecutionProof { message: ExecutionProof { proof_data: VariableList::new(vec![0xaa, 0xbb, 0xcc]).unwrap(), - proof_type: 1, + proof_type, public_input: PublicInput { new_payload_request_root: request_root, }, @@ -936,12 +982,13 @@ pub mod test_utils { let metadata = create_request_metadata(request_root, block_hash, parent_hash, block_number); - // Generate proofs + // Generate proofs with distinct proof types to avoid deduplication. let mut proofs = Vec::new(); for i in 0..proof_count { - proofs.push(create_signed_proof( + proofs.push(create_signed_proof_with_type( request_root, request_root.0[0] as u64 + i as u64, + (i as u8).wrapping_add(1), // types 1, 2, 3, ... (avoid 0) )); } diff --git a/beacon_node/execution_layer/src/engine_api.rs b/beacon_node/execution_layer/src/engine_api.rs index 0424530316f..2c2fe8f7b2e 100644 --- a/beacon_node/execution_layer/src/engine_api.rs +++ b/beacon_node/execution_layer/src/engine_api.rs @@ -26,7 +26,7 @@ pub use types::{ use types::{ ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadDeneb, ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionPayloadGloas, ExecutionRequests, - KzgProofs, + KzgProofs, Slot, }; use types::{GRAFFITI_BYTES_LEN, Graffiti}; @@ -269,6 +269,8 @@ pub struct MissingProofInfo { pub root: Hash256, /// Proof types already received for this request root (to avoid redundant requests). pub existing_proof_types: Vec, + /// Beacon slot of the block whose proofs are missing. + pub slot: Slot, } #[derive(Clone, Debug, PartialEq)] diff --git a/beacon_node/execution_layer/src/engines.rs b/beacon_node/execution_layer/src/engines.rs index 6559ca0e90e..a65cbd00d29 100644 --- a/beacon_node/execution_layer/src/engines.rs +++ b/beacon_node/execution_layer/src/engines.rs @@ -108,6 +108,17 @@ pub struct ForkchoiceState { pub finalized_block_hash: ExecutionBlockHash, } +impl ForkchoiceState { + /// Creates a `ForkchoiceState` with all block hashes set to the genesis hash. + pub fn new_genesis(genesis_hash: ExecutionBlockHash) -> Self { + Self { + head_block_hash: genesis_hash, + safe_block_hash: genesis_hash, + finalized_block_hash: genesis_hash, + } + } +} + #[derive(Hash, PartialEq, std::cmp::Eq)] struct PayloadIdCacheKey { pub head_block_hash: ExecutionBlockHash, diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index fa8916639ae..d29e65ad776 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -1657,7 +1657,7 @@ impl ExecutionLayer { }; let proof_engine_result = if let Some(proof_engine) = self.proof_engine() { - match proof_engine.forkchoice_updated(forkchoice_state).await { + match proof_engine.forkchoice_updated(forkchoice_state) { Ok(response) => Some(Ok(response)), Err(e) => { debug!(error = ?e, "Proof engine forkchoice_updated error (non-fatal)"); diff --git a/beacon_node/execution_layer/src/test_utils/mock_proof_node_client.rs b/beacon_node/execution_layer/src/test_utils/mock_proof_node_client.rs index 569dcef4d71..04654bc809b 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_proof_node_client.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_proof_node_client.rs @@ -55,7 +55,7 @@ use types::{ #[derive(SszEncode, SszDecode, TreeHashDerive)] #[ssz(enum_behaviour = "transparent")] #[tree_hash(enum_behaviour = "transparent")] -pub(crate) struct OwnedNewPayloadRequest { +pub struct OwnedNewPayloadRequest { #[superstruct( only(Bellatrix), partial_getter(rename = "execution_payload_bellatrix") diff --git a/beacon_node/execution_layer/src/test_utils/mod.rs b/beacon_node/execution_layer/src/test_utils/mod.rs index 2f492658515..b8265dbd180 100644 --- a/beacon_node/execution_layer/src/test_utils/mod.rs +++ b/beacon_node/execution_layer/src/test_utils/mod.rs @@ -35,8 +35,8 @@ pub use hook::Hook; pub use mock_builder::{MockBuilder, Operation, mock_builder_extra_data}; pub use mock_execution_layer::MockExecutionLayer; pub use mock_proof_node_client::{ - MockClientEvent, MockProofNodeClient, get_mock_proof_engine, make_test_fulu_ssz, - mock_proof_engine_url, parse_mock_index, register_mock_proof_engine, + MockClientEvent, MockProofNodeClient, OwnedNewPayloadRequest, get_mock_proof_engine, + make_test_fulu_ssz, mock_proof_engine_url, parse_mock_index, register_mock_proof_engine, }; pub const DEFAULT_TERMINAL_DIFFICULTY: u64 = 6400; diff --git a/beacon_node/network/src/sync/proof_sync.rs b/beacon_node/network/src/sync/proof_sync.rs index 516fdaed0c4..b5d673eea4f 100644 --- a/beacon_node/network/src/sync/proof_sync.rs +++ b/beacon_node/network/src/sync/proof_sync.rs @@ -230,6 +230,15 @@ impl ProofSync { .filter(|info| !in_flight_roots.contains(&info.root)) .take(available) { + if peer_slot < info.slot { + debug!( + block_root = %info.root, + slot = %info.slot, + %peer_slot, + "ProofSync: best peer slot behind missing block, skipping" + ); + continue; + } match cx.request_execution_proofs_by_root(peer_id, info.root) { Ok(id) => { debug!( diff --git a/beacon_node/network/src/sync/tests/range.rs b/beacon_node/network/src/sync/tests/range.rs index c50bfe0dced..afda7fddbe2 100644 --- a/beacon_node/network/src/sync/tests/range.rs +++ b/beacon_node/network/src/sync/tests/range.rs @@ -763,6 +763,7 @@ fn missing_proof(root: Hash256) -> MissingProofInfo { MissingProofInfo { root, existing_proof_types: vec![], + slot: Default::default(), } } diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 588a24d0c3d..e781eb6d9ee 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -84,7 +84,7 @@ pub struct HotColdDB, Cold: ItemStore> { /// Kept separate from `block_cache` so it is always available regardless of whether the /// block cache is enabled. Required for proof verification to look up the beacon block root /// associated with an execution payload. - request_root_to_block_root: Mutex>, + request_root_to_block_root: Mutex>, /// EIP-8025: always-on cache mapping block_root -> request_root. /// /// Used by the HTTP API to retrieve the request root for a given block root. @@ -1054,17 +1054,20 @@ impl, Cold: ItemStore> HotColdDB /// Store bidirectional mapping between request_root and block_root (EIP-8025). /// /// This is in-memory only and not persisted to database in the initial implementation. - pub fn put_request_root_mapping(&self, request_root: Hash256, block_root: Hash256) { + pub fn put_request_root_mapping(&self, request_root: Hash256, block_root: Hash256, slot: Slot) { self.request_root_to_block_root .lock() - .put(request_root, block_root); + .put(request_root, (block_root, slot)); self.block_root_to_request_root .lock() .put(block_root, request_root); } - /// Look up block_root by request_root (EIP-8025, cache-only, no database). - pub fn get_block_root_by_request_root(&self, request_root: &Hash256) -> Option { + /// Look up block_root and slot by request_root (EIP-8025, cache-only, no database). + pub fn get_block_root_by_request_root( + &self, + request_root: &Hash256, + ) -> Option<(Hash256, Slot)> { self.request_root_to_block_root .lock() .get(request_root) diff --git a/scripts/local_testnet/kurtosis_zkboost/main.star b/scripts/local_testnet/kurtosis_zkboost/main.star index 31ecc1949c6..f124a0f113d 100644 --- a/scripts/local_testnet/kurtosis_zkboost/main.star +++ b/scripts/local_testnet/kurtosis_zkboost/main.star @@ -29,7 +29,6 @@ ZKBOOST_METRICS_PATH = "/metrics" ZKBOOST_CONFIG_TEMPLATE = """\ port = {port} el_endpoint = "http://{el_service}:{el_rpc_port}" -chain_config_path = "/app/chain_config.json" [[zkvm]] kind = "mock" @@ -38,40 +37,6 @@ mock_proof_size = {mock_proof_size} proof_type = "reth-zisk" """ -# Chain config matching the Kurtosis devnet genesis (chainId 3151908). -# Providing this as a file avoids the debug_chainConfig RPC call, which -# is not available in standard geth. -CHAIN_CONFIG_JSON = """\ -{ - "chainId": 3151908, - "homesteadBlock": 0, - "daoForkSupport": false, - "eip150Block": 0, - "eip155Block": 0, - "eip158Block": 0, - "byzantiumBlock": 0, - "constantinopleBlock": 0, - "petersburgBlock": 0, - "istanbulBlock": 0, - "berlinBlock": 0, - "londonBlock": 0, - "mergeNetsplitBlock": 0, - "shanghaiTime": 0, - "cancunTime": 0, - "pragueTime": 0, - "osakaTime": 0, - "bpo1Time": 0, - "terminalTotalDifficulty": 0, - "terminalTotalDifficultyPassed": true, - "depositContractAddress": "0x00000000219ab540356cbb839cbe05303d7705fa", - "blobSchedule": { - "bpo1": { "baseFeeUpdateFraction": 8346193, "max": 15, "target": 10 }, - "cancun": { "baseFeeUpdateFraction": 3338477, "max": 6, "target": 3 }, - "osaka": { "baseFeeUpdateFraction": 5007716, "max": 9, "target": 6 }, - "prague": { "baseFeeUpdateFraction": 5007716, "max": 9, "target": 6 } - } -} -""" def run(plan, args): """Start ethereum-package then add zkboost-server sidecars.""" @@ -113,10 +78,6 @@ def run(plan, args): template = config_content, data = {}, ), - "chain_config.json": struct( - template = CHAIN_CONFIG_JSON, - data = {}, - ), }, ) diff --git a/scripts/local_testnet/network_params_eip8025_zkboost.yaml b/scripts/local_testnet/network_params_eip8025_zkboost.yaml index 7c5e677222c..ec192a5e9ee 100644 --- a/scripts/local_testnet/network_params_eip8025_zkboost.yaml +++ b/scripts/local_testnet/network_params_eip8025_zkboost.yaml @@ -52,7 +52,7 @@ additional_services: # ── zkboost-server sidecar configuration ───────────────────────────────────── # Processed by kurtosis_zkboost/main.star; NOT forwarded to ethereum-package. zkboost: - image: zkboost-server:local + image: ghcr.io/eth-act/zkboost/zkboost-server:0.3.0 # Each instance connects to one EL node's JSON-RPC endpoint for witness data. instances: - name: zkboost-1 @@ -60,5 +60,5 @@ zkboost: - name: zkboost-2 el_service: el-2-reth-lighthouse # Mock zkVM settings (real zkVM backends need external ere-server instances). - mock_proving_time_ms: 5000 + mock_proving_time_ms: 300 mock_proof_size: 1024 diff --git a/testing/proof_engine_zkboost/Cargo.toml b/testing/proof_engine_zkboost/Cargo.toml index ab3ef2c7b54..c57dd152c2f 100644 --- a/testing/proof_engine_zkboost/Cargo.toml +++ b/testing/proof_engine_zkboost/Cargo.toml @@ -8,6 +8,8 @@ portable = ["types/portable"] [dependencies] anyhow = { workspace = true } +ethereum_ssz = { workspace = true } +tree_hash = { workspace = true } axum = { workspace = true } bytes = { workspace = true } execution_layer = { workspace = true } diff --git a/testing/proof_engine_zkboost/src/lib.rs b/testing/proof_engine_zkboost/src/lib.rs index 5c3a318bcf4..2ba7f4e985d 100644 --- a/testing/proof_engine_zkboost/src/lib.rs +++ b/testing/proof_engine_zkboost/src/lib.rs @@ -23,17 +23,21 @@ pub mod zkboost_harness; mod tests { use crate::zkboost_harness::{FIXTURE_NEW_PAYLOAD_REQUEST, ZkboostTestHarness}; use execution_layer::eip8025::{HttpProofNodeClient, ProofNodeClient, ProofType}; + use execution_layer::test_utils::OwnedNewPayloadRequest; use futures::StreamExt; use sensitive_url::SensitiveUrl; + use ssz::Decode; use std::time::Duration; use tokio::time::timeout; + use tree_hash::TreeHash; + use types::MainnetEthSpec; use types::execution::eip8025::ProofAttributes; use zkboost_types::ProofType as ZkBoostProofType; /// Helper: create an `HttpProofNodeClient` pointing at the test server. fn client_for(url: &str) -> HttpProofNodeClient { let sensitive_url = SensitiveUrl::parse(url).expect("server URL should be valid"); - HttpProofNodeClient::new(sensitive_url, Some(Duration::from_secs(30))) + HttpProofNodeClient::new(sensitive_url, None) } /// The u8 value for `EthrexZisk` (our default test proof type). @@ -60,8 +64,15 @@ mod tests { .await .expect("request_proofs should succeed against real server"); - // The root should be non-zero (the server computes tree_hash_root of the SSZ). - assert!(!root.is_zero(), "returned root should be non-zero"); + let expected_root = + OwnedNewPayloadRequest::::from_ssz_bytes(FIXTURE_NEW_PAYLOAD_REQUEST) + .expect("fixture SSZ should decode to a valid NewPayloadRequest") + .tree_hash_root(); + + assert_eq!( + root, expected_root, + "server root should match tree_hash_root of fixture payload" + ); } // ─── Test 2: SSE events from real server are parsed correctly ──────────── From 74155324acd7261c326e02c91b4b876f2df08596 Mon Sep 17 00:00:00 2001 From: frisitano Date: Wed, 25 Mar 2026 04:49:09 +0100 Subject: [PATCH 3/4] lint: cargo sort --- testing/proof_engine_zkboost/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/proof_engine_zkboost/Cargo.toml b/testing/proof_engine_zkboost/Cargo.toml index c57dd152c2f..3306cfef5b0 100644 --- a/testing/proof_engine_zkboost/Cargo.toml +++ b/testing/proof_engine_zkboost/Cargo.toml @@ -8,10 +8,9 @@ portable = ["types/portable"] [dependencies] anyhow = { workspace = true } -ethereum_ssz = { workspace = true } -tree_hash = { workspace = true } axum = { workspace = true } bytes = { workspace = true } +ethereum_ssz = { workspace = true } execution_layer = { workspace = true } futures = { workspace = true } metrics-exporter-prometheus = { workspace = true } @@ -24,6 +23,7 @@ tokio = { workspace = true } tokio-stream = { workspace = true } tokio-util = { workspace = true } tracing = { workspace = true } +tree_hash = { workspace = true } types = { workspace = true } url = { workspace = true } zkboost-server = { workspace = true } From bf05fb756f57c5b06fbd17f40090f46c3ca5158f Mon Sep 17 00:00:00 2001 From: frisitano Date: Wed, 25 Mar 2026 05:37:22 +0100 Subject: [PATCH 4/4] remove timeout for proof node SSE event subscription --- .../execution_layer/src/eip8025/proof_node_client.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/beacon_node/execution_layer/src/eip8025/proof_node_client.rs b/beacon_node/execution_layer/src/eip8025/proof_node_client.rs index 25af9895479..af974ad617e 100644 --- a/beacon_node/execution_layer/src/eip8025/proof_node_client.rs +++ b/beacon_node/execution_layer/src/eip8025/proof_node_client.rs @@ -117,17 +117,21 @@ enum ProofVerificationStatus { pub struct HttpProofNodeClient { client: Client, url: SensitiveUrl, + timeout: Duration, } impl HttpProofNodeClient { /// Create a new HTTP proof node client. pub fn new(url: SensitiveUrl, timeout: Option) -> Self { let client = Client::builder() - .timeout(timeout.unwrap_or(PROOF_ENGINE_TIMEOUT)) .build() .expect("Failed to build HTTP client"); - Self { client, url } + Self { + client, + url, + timeout: timeout.unwrap_or(PROOF_ENGINE_TIMEOUT), + } } /// Build a URL from the base URL and a path. @@ -164,6 +168,7 @@ impl ProofNodeClient for HttpProofNodeClient { .query(&[(QUERY_PROOF_TYPES, &proof_types_csv)]) .header(HEADER_CONTENT_TYPE, HEADER_VALUE_SSZ) .body(ssz_body) + .timeout(self.timeout) .send() .await? .error_for_status()? @@ -192,6 +197,7 @@ impl ProofNodeClient for HttpProofNodeClient { ]) .header(HEADER_CONTENT_TYPE, HEADER_VALUE_SSZ) .body(proof_data.to_vec()) + .timeout(self.timeout) .send() .await? .error_for_status()? @@ -212,6 +218,7 @@ impl ProofNodeClient for HttpProofNodeClient { Ok(self .client .get(self.url(&format!("{PATH_PROOFS}/{root}/{proof_type_str}"))) + .timeout(self.timeout) .send() .await? .error_for_status()?