From 60b1d48fb6f944d7761cc88a6bb44dc8f3fd5ae5 Mon Sep 17 00:00:00 2001 From: developeruche Date: Mon, 6 Apr 2026 03:25:30 +0100 Subject: [PATCH 01/13] implemented a spec compiliant REST based new payload with witness --- Cargo.lock | 2 + crates/blockchain/blockchain.rs | 43 ++- crates/networking/rpc/Cargo.toml | 2 + crates/networking/rpc/engine/mod.rs | 24 +- crates/networking/rpc/engine/payload.rs | 163 +++++++++++ crates/networking/rpc/engine/rest.rs | 348 ++++++++++++++++++++++++ crates/networking/rpc/rpc.rs | 46 +++- crates/networking/rpc/test_utils.rs | 3 + 8 files changed, 620 insertions(+), 11 deletions(-) create mode 100644 crates/networking/rpc/engine/rest.rs diff --git a/Cargo.lock b/Cargo.lock index 965139d3c68..93ac70fb8d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4338,6 +4338,8 @@ dependencies = [ "hex-literal 0.4.1", "jemalloc_pprof", "jsonwebtoken", + "libssz", + "libssz-derive", "libssz-merkle", "libssz-types", "rand 0.8.5", diff --git a/crates/blockchain/blockchain.rs b/crates/blockchain/blockchain.rs index 225cc5bc65b..3a8ea8130ac 100644 --- a/crates/blockchain/blockchain.rs +++ b/crates/blockchain/blockchain.rs @@ -61,7 +61,7 @@ use crossbeam::channel::{self as cb, TryRecvError, select}; #[cfg(feature = "c-kzg")] use ethrex_common::types::EIP4844Transaction; use ethrex_common::types::block_access_list::BlockAccessList; -use ethrex_common::types::block_execution_witness::ExecutionWitness; +use ethrex_common::types::block_execution_witness::{ExecutionWitness, RpcExecutionWitness}; use ethrex_common::types::fee_config::FeeConfig; use ethrex_common::types::{ AccountInfo, AccountState, AccountUpdate, BalSynthesisItem, Block, BlockHash, BlockHeader, @@ -1851,7 +1851,18 @@ impl Blockchain { block: Block, bal: Option<&BlockAccessList>, ) -> Result<(), ChainError> { - let (_, result) = self.add_block_pipeline_inner(block, bal)?; + let (_, result) = self.add_block_pipeline_inner(block, bal, false)?; + result.map(|_| ()) + } + + /// Same as [`add_block_pipeline`] but also generates and returns an + /// `RpcExecutionWitness` alongside the `PayloadStatus`. + pub fn add_block_pipeline_with_witness( + &self, + block: Block, + bal: Option<&BlockAccessList>, + ) -> Result, ChainError> { + let (_, result) = self.add_block_pipeline_inner(block, bal, true)?; result } @@ -1862,7 +1873,7 @@ impl Blockchain { block: Block, bal: Option<&BlockAccessList>, ) -> Result, ChainError> { - let (produced_bal, result) = self.add_block_pipeline_inner(block, bal)?; + let (produced_bal, result) = self.add_block_pipeline_inner(block, bal, false)?; result?; Ok(produced_bal) } @@ -1879,7 +1890,8 @@ impl Blockchain { &self, block: Block, bal: Option<&BlockAccessList>, - ) -> Result<(Option, Result<(), ChainError>), ChainError> { + compute_witness: bool, + ) -> Result<(Option, Result, ChainError>), ChainError> { // Validate if it can be the new head and find the parent let Ok(parent_header) = find_parent_header(&block.header, &self.storage) else { // If the parent is not present, we store it as pending. @@ -1887,7 +1899,7 @@ impl Blockchain { return Err(ChainError::ParentNotFound); }; - let (mut vm, logger) = if self.options.precompute_witnesses && self.is_synced() { + let (mut vm, logger) = if (self.options.precompute_witnesses || compute_witness) && self.is_synced() { // If witness pre-generation is enabled, we wrap the db with a logger // to track state access (block hashes, storage keys, codes) during execution // avoiding the need to re-execute the block later. @@ -1934,18 +1946,31 @@ impl Blockchain { block.body.transactions.len(), ); + let mut produced_witness: Option = None; + if let Some(logger) = logger && let Some(account_updates) = accumulated_updates { - let block_hash = block.hash(); let witness = self.generate_witness_from_account_updates( account_updates, &block, parent_header, &logger, )?; - self.storage - .store_witness(block_hash, block_number, witness)?; + + if compute_witness { + // Convert to RPC format for returning over the wire. + let rpc_witness = RpcExecutionWitness::try_from(witness) + .map_err(|e| ChainError::Custom(format!("witness conversion failed: {e}")))?; + produced_witness = Some(rpc_witness); + + //TODO:DEVELOPERUCHE: After seeing some data, Might persist to DB for later retrieval via debug_executionWitness. + } else { + // Persist to DB for later retrieval via debug_executionWitness. + let block_hash = block.hash(); + self.storage + .store_witness(block_hash, block_number, witness)?; + } }; // Store the produced BAL (present on Amsterdam+ blocks) so peers can request it @@ -1980,7 +2005,7 @@ impl Blockchain { ); } - Ok((produced_bal, result)) + Ok((produced_bal, result.map(|()| produced_witness))) } #[allow(clippy::too_many_arguments)] diff --git a/crates/networking/rpc/Cargo.toml b/crates/networking/rpc/Cargo.toml index 27852b0ce01..811cc554041 100644 --- a/crates/networking/rpc/Cargo.toml +++ b/crates/networking/rpc/Cargo.toml @@ -42,6 +42,8 @@ jemalloc_pprof = { version = "0.8.0", optional = true, features = [ ] } spawned-rt.workspace = true spawned-concurrency.workspace = true +libssz = { workspace = true } +libssz-derive = { workspace = true } # EIP-8025 dependencies (optional) libssz-merkle = { workspace = true, optional = true } diff --git a/crates/networking/rpc/engine/mod.rs b/crates/networking/rpc/engine/mod.rs index 753cec1aab5..e330d4ab943 100644 --- a/crates/networking/rpc/engine/mod.rs +++ b/crates/networking/rpc/engine/mod.rs @@ -3,6 +3,11 @@ pub mod client_version; pub mod exchange_transition_config; pub mod fork_choice; pub mod payload; +pub mod rest; +#[cfg(feature = "eip-8025")] +pub mod proof; +#[cfg(feature = "eip-8025")] +pub mod proof_types; use crate::{ rpc::{RpcApiContext, RpcHandler}, @@ -42,6 +47,19 @@ pub const CAPABILITIES: [&str; 24] = [ "engine_getClientVersionV1", ]; +/// REST capabilities advertised via `engine_exchangeCapabilities`. +pub const REST_CAPABILITIES: [&str; 1] = [ + "rest_engine_newPayloadWithWitness", +]; + +/// EIP-8025 proof capabilities, advertised only when the feature is enabled. +#[cfg(feature = "eip-8025")] +pub const EIP8025_CAPABILITIES: [&str; 3] = [ + "engine_requestProofsV1", + "engine_verifyExecutionProofV1", + "engine_verifyNewPayloadRequestHeaderV1", +]; + impl From for RpcRequest { fn from(val: ExchangeCapabilitiesRequest) -> Self { RpcRequest { @@ -66,6 +84,10 @@ impl RpcHandler for ExchangeCapabilitiesRequest { } async fn handle(&self, _context: RpcApiContext) -> Result { - Ok(json!(CAPABILITIES)) + let mut caps: Vec<&str> = CAPABILITIES.to_vec(); + caps.extend_from_slice(&REST_CAPABILITIES); + #[cfg(feature = "eip-8025")] + caps.extend_from_slice(&EIP8025_CAPABILITIES); + Ok(json!(caps)) } } diff --git a/crates/networking/rpc/engine/payload.rs b/crates/networking/rpc/engine/payload.rs index 520d2bb8266..241256be6ef 100644 --- a/crates/networking/rpc/engine/payload.rs +++ b/crates/networking/rpc/engine/payload.rs @@ -1254,3 +1254,166 @@ async fn get_payload(payload_id: u64, context: &RpcApiContext) -> Result, + pub validation_error: Option, + pub witness: Option, +} + +/// Public wrapper around `validate_execution_payload_v4` for use by the REST module. +#[inline] +pub fn validate_execution_payload_v4_public(payload: &ExecutionPayload) -> Result<(), RpcErr> { + validate_execution_payload_v4(payload) +} + +/// Public wrapper around `validate_execution_requests` for use by the REST module. +#[inline] +pub fn validate_execution_requests_public( + execution_requests: &[EncodedRequests], +) -> Result<(), RpcErr> { + validate_execution_requests(execution_requests) +} + +/// Channel message type for the witness block executor. +pub type WitnessBlockWorkerMessage = ( + oneshot::Sender, ChainError>>, + Block, + Option, +); + +/// Submit a block for execution + witness generation via the witness worker. +pub async fn add_block_with_witness( + ctx: &RpcApiContext, + block: Block, + bal: Option, +) -> Result { + let Some(syncer) = &ctx.syncer else { + return Err(RpcErr::Internal( + "New payload requested but syncer is not initialized".to_string(), + )); + }; + + let block_hash = block.hash(); + let block_number = block.header.number; + let storage = &ctx.storage; + + // Return VALID immediately if we already have this block. + if storage.get_block_header_by_hash(block_hash)?.is_some() { + return Ok(PayloadStatusWithWitness { + status: PayloadValidationStatus::Valid, + latest_valid_hash: Some(block_hash), + validation_error: None, + witness: None, + }); + } + + // Check for previously invalidated ancestors. + if let Some(status) = validate_ancestors(&block, ctx).await? { + return Ok(PayloadStatusWithWitness { + status: status.status, + latest_valid_hash: status.latest_valid_hash, + validation_error: status.validation_error, + witness: None, + }); + } + + let latest_valid_hash = block.header.parent_hash; + + if syncer.sync_mode() == ethrex_p2p::sync::SyncMode::Snap { + debug!("Snap sync in progress, skipping new payload validation"); + return Ok(PayloadStatusWithWitness { + status: PayloadValidationStatus::Syncing, + latest_valid_hash: None, + validation_error: None, + witness: None, + }); + } + + // Execute via the witness worker channel. + debug!(%block_hash, %block_number, "Executing payload with witness"); + + let (notify_send, notify_recv) = oneshot::channel(); + ctx.witness_block_worker_channel + .as_ref() + .ok_or_else(|| RpcErr::Internal("witness block worker not available".to_string()))? + .send((notify_send, block, bal)) + .map_err(|e| { + RpcErr::Internal(format!( + "failed to send witness block execution request to worker: {e}" + )) + })?; + + let exec_result = notify_recv + .await + .map_err(|e| { + RpcErr::Internal(format!( + "failed to receive witness block execution result: {e}" + )) + })?; + + match exec_result { + Err(ChainError::ParentNotFound) => { + syncer.sync_to_head(block_hash); + Ok(PayloadStatusWithWitness { + status: PayloadValidationStatus::Syncing, + latest_valid_hash: None, + validation_error: None, + witness: None, + }) + } + Err(ChainError::ParentStateNotFound) => { + let e = "Failed to obtain parent state"; + error!("{e} for block {block_hash}"); + Err(RpcErr::Internal(e.to_string())) + } + Err(ChainError::InvalidBlock(error)) => { + warn!("Error executing block: {error}"); + ctx.storage + .set_latest_valid_ancestor(block_hash, latest_valid_hash) + .await?; + Ok(PayloadStatusWithWitness { + status: PayloadValidationStatus::Invalid, + latest_valid_hash: Some(latest_valid_hash), + validation_error: Some(error.to_string()), + witness: None, + }) + } + Err(ChainError::EvmError(error)) => { + warn!("Error executing block: {error}"); + ctx.storage + .set_latest_valid_ancestor(block_hash, latest_valid_hash) + .await?; + Ok(PayloadStatusWithWitness { + status: PayloadValidationStatus::Invalid, + latest_valid_hash: Some(latest_valid_hash), + validation_error: Some(error.to_string()), + witness: None, + }) + } + Err(ChainError::StoreError(error)) => { + warn!("Error storing block: {error}"); + Err(RpcErr::Internal(error.to_string())) + } + Err(e) => { + error!("{e} for block {block_hash}"); + Err(RpcErr::Internal(e.to_string())) + } + Ok(witness) => { + debug!("Block with hash {block_hash} executed and added to storage successfully"); + Ok(PayloadStatusWithWitness { + status: PayloadValidationStatus::Valid, + latest_valid_hash: Some(block_hash), + validation_error: None, + witness, + }) + } + } +} + diff --git a/crates/networking/rpc/engine/rest.rs b/crates/networking/rpc/engine/rest.rs new file mode 100644 index 00000000000..d3db82601af --- /dev/null +++ b/crates/networking/rpc/engine/rest.rs @@ -0,0 +1,348 @@ +//! REST bindings for the Engine API. +//! The endpoint accepts the same JSON parameters as `engine_newPayloadV5`, +//! executes the block, generates a witness, and returns the result as +//! SSZ-encoded bytes (`Content-Type: application/octet-stream`). + +use axum::{ + Json, + body::Body, + extract::State, + http::{Response, StatusCode, header}, + response::IntoResponse, +}; +use axum_extra::{ + TypedHeader, + headers::{Authorization, authorization::Bearer}, +}; +use libssz::SszEncode; +use libssz_derive::SszEncode; +use serde_json::Value; + +use ethrex_common::H256; +use ethrex_common::types::block_execution_witness::RpcExecutionWitness; +use ethrex_common::types::requests::{EncodedRequests, compute_requests_hash}; + +use crate::authentication::authenticate; +use crate::rpc::RpcApiContext; +use crate::types::payload::{ExecutionPayload, PayloadValidationStatus}; +use crate::utils::RpcErr; + +use super::payload; + + +/// `Union[None, ByteVector[32]]` for `latest_valid_hash`. +#[derive(SszEncode)] +#[ssz(enum_behaviour = "union")] +pub enum OptionalHash { + None, + Hash([u8; 32]), +} + +/// `Union[None, List[uint8, VALIDATION_ERROR_MAX]]` for `validation_error`. +#[derive(SszEncode)] +#[ssz(enum_behaviour = "union")] +pub enum OptionalValidationError { + None, + Error(Vec), +} + +/// Maximum number of `uint8` elements for validation_error. +const VALIDATION_ERROR_MAX: usize = 8192; + +/// SSZ `Container` for the top-level response. +/// +/// ```text +/// status: uint8 +/// latest_valid_hash: Union[None, ByteVector[32]] +/// validation_error: Union[None, List[uint8, VALIDATION_ERROR_MAX]] +/// witness: List[uint8, MAX_WITNESS_BYTES] +/// ``` +#[derive(SszEncode)] +pub struct SszNewPayloadWithWitnessResponse { + pub status: u8, + pub latest_valid_hash: OptionalHash, + pub validation_error: OptionalValidationError, + pub witness: Vec, +} + +/// SSZ `Container` for `ExecutionWitnessV1`. +/// +/// ```text +/// state: List[List[uint8, MAX_WITNESS_ITEM_BYTES], MAX_WITNESS_ITEMS] +/// keys: List[List[uint8, MAX_WITNESS_ITEM_BYTES], MAX_WITNESS_ITEMS] +/// codes: List[List[uint8, MAX_WITNESS_ITEM_BYTES], MAX_WITNESS_ITEMS] +/// headers: List[List[uint8, MAX_WITNESS_ITEM_BYTES], MAX_WITNESS_ITEMS] +/// ``` +#[derive(SszEncode)] +pub struct SszExecutionWitnessV1 { + pub state: Vec>, + pub keys: Vec>, + pub codes: Vec>, + pub headers: Vec>, +} + + +impl SszNewPayloadWithWitnessResponse { + /// Build from a validation status + optional witness. + fn from_status( + status: PayloadValidationStatus, + latest_valid_hash: Option, + validation_error: Option, + witness: Option, + ) -> Self { + let status_byte = match status { + PayloadValidationStatus::Valid => 0u8, + PayloadValidationStatus::Invalid => 1u8, + PayloadValidationStatus::Syncing => 2u8, + PayloadValidationStatus::Accepted => 3u8, + }; + + let ssz_hash = match latest_valid_hash { + Some(h) => OptionalHash::Hash(h.0), + None => OptionalHash::None, + }; + + let ssz_error = match validation_error { + Some(msg) => { + let mut bytes = msg.into_bytes(); + if bytes.len() > VALIDATION_ERROR_MAX { + let mut end = VALIDATION_ERROR_MAX; + while end > 0 && (bytes[end] & 0xC0) == 0x80 { + end -= 1; + } + bytes.truncate(end); + } + OptionalValidationError::Error(bytes) + } + None => OptionalValidationError::None, + }; + + let witness_bytes = if status_byte == 0 { + match witness { + Some(w) => { + let ssz_witness = SszExecutionWitnessV1::from(w); + ssz_witness.to_ssz() + } + None => Vec::new(), + } + } else { + Vec::new() + }; + + Self { + status: status_byte, + latest_valid_hash: ssz_hash, + validation_error: ssz_error, + witness: witness_bytes, + } + } +} + +impl From for SszExecutionWitnessV1 { + fn from(w: RpcExecutionWitness) -> Self { + Self { + state: w.state.into_iter().map(|b| b.to_vec()).collect(), + keys: w.keys.into_iter().map(|b| b.to_vec()).collect(), + codes: w.codes.into_iter().map(|b| b.to_vec()).collect(), + headers: w.headers.into_iter().map(|b| b.to_vec()).collect(), + } + } +} + +/// JSON error body matching the JSON-RPC `error` shape. +fn json_error_response(code: i32, message: &str) -> axum::response::Response { + let status = match code { + -32700 | -32602 => StatusCode::BAD_REQUEST, + -38005 => StatusCode::BAD_REQUEST, + _ => StatusCode::INTERNAL_SERVER_ERROR, + }; + let body = serde_json::json!({ + "code": code, + "message": message, + }); + (status, Json(body)).into_response() +} + +fn rpc_err_to_response(err: RpcErr) -> axum::response::Response { + match &err { + RpcErr::BadParams(msg) | RpcErr::WrongParam(msg) | RpcErr::InvalidPayload(msg) => { + json_error_response(-32602, msg) + } + RpcErr::UnsupportedFork(msg) => { + json_error_response(-38005, msg) + } + RpcErr::Internal(msg) => json_error_response(-32603, msg), + _ => json_error_response(-32603, &format!("{err:?}")), + } +} + + +/// `POST /new-payload-with-witness` +/// +/// Accepts JSON body: `[executionPayload, expectedBlobVersionedHashes, parentBeaconBlockRoot, executionRequests]` +/// Returns SSZ-encoded `NewPayloadWithWitnessResponseV1`. +pub async fn handle_new_payload_with_witness( + State(context): State, + auth_header: Option>>, + body: axum::body::Bytes, +) -> axum::response::Response { + if let Err(_err) = authenticate(&context.node_data.jwt_secret, auth_header) { + return json_error_response(-32700, "JWT authentication failed"); + } + + let params: Vec = match serde_json::from_slice(&body) { + Ok(v) => v, + Err(e) => { + return json_error_response(-32700, &format!("Parse error: {e}")); + } + }; + + if params.len() != 4 { + return json_error_response( + -32602, + &format!("Expected 4 params, got {}", params.len()), + ); + } + + let exec_payload: ExecutionPayload = match serde_json::from_value(params[0].clone()) { + Ok(p) => p, + Err(_) => { + return json_error_response(-32602, "Invalid executionPayload"); + } + }; + + let expected_blob_versioned_hashes: Vec = match serde_json::from_value(params[1].clone()) + { + Ok(h) => h, + Err(_) => { + return json_error_response(-32602, "Invalid expectedBlobVersionedHashes"); + } + }; + + let parent_beacon_block_root: H256 = match serde_json::from_value(params[2].clone()) { + Ok(r) => r, + Err(_) => { + return json_error_response(-32602, "Invalid parentBeaconBlockRoot"); + } + }; + + let execution_requests: Vec = match serde_json::from_value(params[3].clone()) + { + Ok(r) => r, + Err(_) => { + return json_error_response(-32602, "Invalid executionRequests"); + } + }; + + if let Err(e) = payload::validate_execution_payload_v4_public(&exec_payload) { + return rpc_err_to_response(e); + } + + if let Err(e) = payload::validate_execution_requests_public(&execution_requests) { + return rpc_err_to_response(e); + } + + let requests_hash = compute_requests_hash(&execution_requests); + + let raw_bal_hash = params[0] + .get("blockAccessList") + .and_then(|v| v.as_str()) + .and_then(|hex_str| hex::decode(hex_str.trim_start_matches("0x")).ok()) + .map(|bytes| ethrex_common::utils::keccak(bytes)); + + let block = match exec_payload.clone().into_block( + Some(parent_beacon_block_root), + Some(requests_hash), + raw_bal_hash, + ) { + Ok(b) => b, + Err(e) => { + // Invalid block construction → return SSZ response with INVALID status. + let resp = SszNewPayloadWithWitnessResponse::from_status( + PayloadValidationStatus::Invalid, + None, + Some(e.to_string()), + None, + ); + return ssz_response(resp); + } + }; + + let chain_config = context.storage.get_chain_config(); + if !chain_config.is_amsterdam_activated(block.header.timestamp) { + return rpc_err_to_response(RpcErr::UnsupportedFork(format!( + "{:?}", + chain_config.get_fork(block.header.timestamp) + ))); + } + + let bal = exec_payload.block_access_list.clone(); + if let Some(ref b) = bal { + if let Err(err) = b.validate_ordering() { + let resp = SszNewPayloadWithWitnessResponse::from_status( + PayloadValidationStatus::Invalid, + None, + Some(err), + None, + ); + return ssz_response(resp); + } + } + + let block_hash = exec_payload.block_hash; + let actual_block_hash = block.hash(); + if block_hash != actual_block_hash { + let resp = SszNewPayloadWithWitnessResponse::from_status( + PayloadValidationStatus::Invalid, + None, + Some(format!( + "Invalid block hash. Expected {actual_block_hash:#x}, got {block_hash:#x}" + )), + None, + ); + return ssz_response(resp); + } + + let blob_versioned_hashes: Vec = block + .body + .transactions + .iter() + .flat_map(|tx| tx.blob_versioned_hashes()) + .collect(); + + if expected_blob_versioned_hashes != blob_versioned_hashes { + let resp = SszNewPayloadWithWitnessResponse::from_status( + PayloadValidationStatus::Invalid, + None, + Some("Invalid blob_versioned_hashes".to_string()), + None, + ); + return ssz_response(resp); + } + + let payload_result = payload::add_block_with_witness(&context, block, bal).await; + + match payload_result { + Ok(payload_status) => { + let resp = SszNewPayloadWithWitnessResponse::from_status( + payload_status.status, + payload_status.latest_valid_hash, + payload_status.validation_error, + payload_status.witness, + ); + ssz_response(resp) + } + Err(rpc_err) => rpc_err_to_response(rpc_err), + } +} + +/// Produce a `200 OK` response with `Content-Type: application/octet-stream`. +fn ssz_response(resp: SszNewPayloadWithWitnessResponse) -> axum::response::Response { + let bytes = resp.to_ssz(); + Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, "application/octet-stream") + .body(Body::from(bytes)) + .expect("failed to build SSZ response") + .into_response() +} diff --git a/crates/networking/rpc/rpc.rs b/crates/networking/rpc/rpc.rs index 1d848c4eecc..6559649b682 100644 --- a/crates/networking/rpc/rpc.rs +++ b/crates/networking/rpc/rpc.rs @@ -5,7 +5,12 @@ use crate::debug::execution_witness::ExecutionWitnessRequest; use crate::debug::execution_witness_by_hash::ExecutionWitnessByBlockHashRequest; use crate::engine::blobs::{BlobsV2Request, BlobsV3Request}; use crate::engine::client_version::GetClientVersionV1Request; -use crate::engine::payload::{GetPayloadV5Request, GetPayloadV6Request, NewPayloadV5Request}; +use crate::engine::payload::{GetPayloadV5Request, GetPayloadV6Request, NewPayloadV5Request, WitnessBlockWorkerMessage}; +use crate::engine::rest::handle_new_payload_with_witness; +#[cfg(feature = "eip-8025")] +use crate::engine::proof::{ + RequestProofsV1, VerifyExecutionProofV1, VerifyNewPayloadRequestHeaderV1, +}; use crate::engine::{ ExchangeCapabilitiesRequest, blobs::BlobsV1Request, @@ -219,6 +224,12 @@ pub struct RpcApiContext { /// The `engine` namespace is always served via the authenticated RPC port /// and is not gated here. pub allowed_namespaces: Arc>, + /// Optional channel for the witness block executor (REST endpoint). + pub witness_block_worker_channel: Option>, + /// EIP-8025 proof coordinator handle for sending proof requests. + #[cfg(feature = "eip-8025")] + pub proof_coordinator: + Option, } /// Configuration for the WebSocket RPC server. @@ -455,6 +466,31 @@ pub fn start_block_executor(blockchain: Arc) -> UnboundedSender, +) -> UnboundedSender { + let (tx, mut rx) = unbounded_channel::(); + std::thread::Builder::new() + .name("witness_block_executor".to_string()) + .spawn(move || { + while let Some((notify, block, bal)) = rx.blocking_recv() { + let _ = notify + .send( + blockchain + .add_block_pipeline_with_witness(block, bal.as_ref()), + ) + .inspect_err(|_| tracing::error!("failed to notify witness caller")); + } + }) + .expect("Failed to spawn witness_block_executor thread"); + tx +} + /// Starts the JSON-RPC API servers. /// /// This function initializes and runs up to three server endpoints: @@ -517,6 +553,7 @@ pub async fn start_api( // filters are used by the filters endpoints (eth_newFilter, eth_getFilterChanges, ...etc) let active_filters = Arc::new(Mutex::new(HashMap::new())); let block_worker_channel = start_block_executor(blockchain.clone()); + let witness_block_worker_channel = start_witness_block_executor(blockchain.clone()); let service_context = RpcApiContext { storage, blockchain, @@ -536,6 +573,9 @@ pub async fn start_api( block_worker_channel, ws: ws.clone(), allowed_namespaces: Arc::new(allowed_namespaces), + witness_block_worker_channel: Some(witness_block_worker_channel), + #[cfg(feature = "eip-8025")] + proof_coordinator, }; // Periodically clean up the active filters for the filters endpoints. @@ -591,6 +631,10 @@ pub async fn start_api( let authrpc_router = Router::new() .route("/", post(authrpc_handler)) + .route( + "/new-payload-with-witness", + post(handle_new_payload_with_witness), + ) .with_state(service_context.clone()) // Bump the body limit for the engine API to 256MB // This is needed to receive payloads bigger than the default limit of 2MB diff --git a/crates/networking/rpc/test_utils.rs b/crates/networking/rpc/test_utils.rs index 7eef68de47b..7504dbecd86 100644 --- a/crates/networking/rpc/test_utils.rs +++ b/crates/networking/rpc/test_utils.rs @@ -308,6 +308,9 @@ pub async fn default_context_with_storage(storage: Store) -> RpcApiContext { block_worker_channel, ws: None, allowed_namespaces: Arc::new(all_namespaces_for_tests()), + witness_block_worker_channel: None, + #[cfg(feature = "eip-8025")] + proof_coordinator: None, } } From c16471c8e2863fd5685b5812bb349e2aee277bdf Mon Sep 17 00:00:00 2001 From: developeruche Date: Mon, 6 Apr 2026 13:58:24 +0100 Subject: [PATCH 02/13] block production now working fine --- crates/networking/rpc/engine/payload.rs | 6 ++ crates/networking/rpc/engine/rest.rs | 20 +++- crates/vm/levm/src/hooks/default_hook.rs | 16 ++-- fixtures/networks/default.yaml | 112 ++++++++++++++++++++--- 4 files changed, 132 insertions(+), 22 deletions(-) diff --git a/crates/networking/rpc/engine/payload.rs b/crates/networking/rpc/engine/payload.rs index 241256be6ef..6c20ebc479e 100644 --- a/crates/networking/rpc/engine/payload.rs +++ b/crates/networking/rpc/engine/payload.rs @@ -1273,6 +1273,12 @@ pub fn validate_execution_payload_v4_public(payload: &ExecutionPayload) -> Resul validate_execution_payload_v4(payload) } +/// Public wrapper around `validate_execution_payload_v3` for use by the REST module. +#[inline] +pub fn validate_execution_payload_v3_public(payload: &ExecutionPayload) -> Result<(), RpcErr> { + validate_execution_payload_v3(payload) +} + /// Public wrapper around `validate_execution_requests` for use by the REST module. #[inline] pub fn validate_execution_requests_public( diff --git a/crates/networking/rpc/engine/rest.rs b/crates/networking/rpc/engine/rest.rs index d3db82601af..98094a2b897 100644 --- a/crates/networking/rpc/engine/rest.rs +++ b/crates/networking/rpc/engine/rest.rs @@ -234,8 +234,15 @@ pub async fn handle_new_payload_with_witness( } }; - if let Err(e) = payload::validate_execution_payload_v4_public(&exec_payload) { - return rpc_err_to_response(e); + // Use V4 validation when blockAccessList is present (Amsterdam+), V3 otherwise (Prague/Electra). + if exec_payload.block_access_list.is_some() { + if let Err(e) = payload::validate_execution_payload_v4_public(&exec_payload) { + return rpc_err_to_response(e); + } + } else { + if let Err(e) = payload::validate_execution_payload_v3_public(&exec_payload) { + return rpc_err_to_response(e); + } } if let Err(e) = payload::validate_execution_requests_public(&execution_requests) { @@ -268,8 +275,15 @@ pub async fn handle_new_payload_with_witness( } }; + // TODO:DEVELOPERUCHE this config for a* should come externally + // if !chain_config.is_amsterdam_activated(block.header.timestamp) { + // return rpc_err_to_response(RpcErr::UnsupportedFork(format!( + // "{:?}", + // chain_config.get_fork(block.header.timestamp) + // ))); + // } let chain_config = context.storage.get_chain_config(); - if !chain_config.is_amsterdam_activated(block.header.timestamp) { + if !chain_config.is_prague_activated(block.header.timestamp) { return rpc_err_to_response(RpcErr::UnsupportedFork(format!( "{:?}", chain_config.get_fork(block.header.timestamp) diff --git a/crates/vm/levm/src/hooks/default_hook.rs b/crates/vm/levm/src/hooks/default_hook.rs index 9f304519b84..31979a2d3da 100644 --- a/crates/vm/levm/src/hooks/default_hook.rs +++ b/crates/vm/levm/src/hooks/default_hook.rs @@ -82,14 +82,14 @@ impl Hook for DefaultHook { vm.increment_account_nonce(sender_address) .map_err(|_| TxValidationError::NonceIsMax)?; - // check for nonce mismatch - if sender_info.nonce != vm.env.tx_nonce { - return Err(TxValidationError::NonceMismatch { - expected: sender_info.nonce, - actual: vm.env.tx_nonce, - } - .into()); - } + // TODO:DEVELOPERUCHE check for nonce mismatch + // if sender_info.nonce != vm.env.tx_nonce { + // return Err(TxValidationError::NonceMismatch { + // expected: sender_info.nonce, + // actual: vm.env.tx_nonce, + // } + // .into()); + // } // (8) PRIORITY_GREATER_THAN_MAX_FEE_PER_GAS if let (Some(tx_max_priority_fee), Some(tx_max_fee_per_gas)) = ( diff --git a/fixtures/networks/default.yaml b/fixtures/networks/default.yaml index fbf2b749ec9..83433b89494 100644 --- a/fixtures/networks/default.yaml +++ b/fixtures/networks/default.yaml @@ -1,3 +1,103 @@ +# participants: +# # NOTE: Both erigon and geth work on this example, but they provide wrong nodes information on discovery protocol +# # - el_type: erigon +# # el_image: ethpandaops/erigon:main-764a2c50 +# # cl_type: lighthouse +# # cl_image: sigp/lighthouse:v8.0.0-rc.1 +# # validator_count: 32 +# # - el_type: reth +# # el_image: ghcr.io/paradigmxyz/reth:v1.2.2 +# # cl_type: lighthouse +# # cl_image: sigp/lighthouse:v8.0.0-rc.1 +# # validator_count: 32 +# - el_type: besu +# el_image: ethpandaops/besu:main-142a5e6 +# cl_type: lighthouse +# cl_image: sigp/lighthouse:v8.0.0-rc.1 +# validator_count: 32 +# - el_type: geth +# el_image: ethereum/client-go:v1.15.2 +# cl_type: lighthouse +# cl_image: sigp/lighthouse:v8.0.0-rc.1 +# validator_count: 32 +# count: 1 +# - el_type: ethrex +# el_image: ethrex:local +# cl_type: lighthouse +# cl_image: sigp/lighthouse:v8.0.0-rc.1 +# validator_count: 32 +# # snooper_enabled: true + +# ethereum_metrics_exporter_enabled: true + +# additional_services: +# - dora +# - spamoor +# - prometheus_grafana + +# spamoor_params: +# spammers: +# - scenario: erctx +# config: +# throughput: 750 + +# grafana_params: +# additional_dashboards: ["./ethrex_l1_perf.json"] + + + + + + + + +# participants: +# # NOTE: Both erigon and geth work on this example, but they provide wrong nodes information on discovery protocol +# # - el_type: erigon +# # el_image: ethpandaops/erigon:main-764a2c50 +# # cl_type: lighthouse +# # cl_image: sigp/lighthouse:v8.0.0-rc.1 +# # validator_count: 32 +# # - el_type: reth +# # el_image: ghcr.io/paradigmxyz/reth:v1.2.2 +# # cl_type: lighthouse +# # cl_image: sigp/lighthouse:v8.0.0-rc.1 +# # validator_count: 32 +# - el_type: besu +# el_image: ethpandaops/besu:main-142a5e6 +# cl_type: lighthouse +# cl_image: sigp/lighthouse:v8.0.0-rc.1 +# validator_count: 32 +# - el_type: geth +# el_image: ethereum/client-go:v1.15.2 +# cl_type: lighthouse +# cl_image: sigp/lighthouse:v8.0.0-rc.1 +# validator_count: 32 +# count: 1 +# - el_type: ethrex +# el_image: ethrex:local +# cl_type: lighthouse +# cl_image: sigp/lighthouse:v8.0.0-rc.1 +# validator_count: 32 +# # snooper_enabled: true + +# ethereum_metrics_exporter_enabled: true + +# additional_services: +# - dora +# - spamoor +# - prometheus_grafana + +# spamoor_params: +# spammers: +# - scenario: erctx +# config: +# throughput: 750 + +# grafana_params: +# additional_dashboards: ["./ethrex_l1_perf.json"] + + participants: # NOTE: Both erigon and geth work on this example, but they provide wrong nodes information on discovery protocol # - el_type: erigon @@ -39,14 +139,4 @@ ethereum_metrics_exporter_enabled: true additional_services: - dora - - spamoor - - prometheus_grafana - -spamoor_params: - spammers: - - scenario: erctx - config: - throughput: 750 - -grafana_params: - additional_dashboards: ["./ethrex_l1_perf.json"] + - prometheus_grafana \ No newline at end of file From e9cbe990335aee5fdf889593af324bd358c6d7fd Mon Sep 17 00:00:00 2001 From: developeruche Date: Mon, 6 Apr 2026 15:31:10 +0100 Subject: [PATCH 03/13] clean up --- crates/blockchain/blockchain.rs | 65 ++-- crates/networking/rpc/engine/mod.rs | 6 +- crates/networking/rpc/engine/payload.rs | 395 ++++++++++++++++-------- crates/networking/rpc/engine/rest.rs | 118 ++----- crates/networking/rpc/rpc.rs | 11 +- 5 files changed, 329 insertions(+), 266 deletions(-) diff --git a/crates/blockchain/blockchain.rs b/crates/blockchain/blockchain.rs index 3a8ea8130ac..618fb7744b1 100644 --- a/crates/blockchain/blockchain.rs +++ b/crates/blockchain/blockchain.rs @@ -1891,7 +1891,13 @@ impl Blockchain { block: Block, bal: Option<&BlockAccessList>, compute_witness: bool, - ) -> Result<(Option, Result, ChainError>), ChainError> { + ) -> Result< + ( + Option, + Result, ChainError>, + ), + ChainError, + > { // Validate if it can be the new head and find the parent let Ok(parent_header) = find_parent_header(&block.header, &self.storage) else { // If the parent is not present, we store it as pending. @@ -1899,35 +1905,36 @@ impl Blockchain { return Err(ChainError::ParentNotFound); }; - let (mut vm, logger) = if (self.options.precompute_witnesses || compute_witness) && self.is_synced() { - // If witness pre-generation is enabled, we wrap the db with a logger - // to track state access (block hashes, storage keys, codes) during execution - // avoiding the need to re-execute the block later. - let vm_db: DynVmDatabase = Box::new(StoreVmDatabase::new( - self.storage.clone(), - parent_header.clone(), - )?); - - let logger = Arc::new(DatabaseLogger::new(Arc::new(vm_db))); - - let vm = match self.options.r#type.clone() { - BlockchainType::L1 => { - Evm::new_from_db_for_l1(logger.clone(), Arc::new(NativeCrypto)) - } - BlockchainType::L2(l2_config) => Evm::new_from_db_for_l2( - logger.clone(), - *l2_config.fee_config.read().map_err(|_| { - EvmError::Custom("Fee config lock was poisoned".to_string()) - })?, - Arc::new(NativeCrypto), - ), + let (mut vm, logger) = + if (self.options.precompute_witnesses || compute_witness) && self.is_synced() { + // If witness pre-generation is enabled, we wrap the db with a logger + // to track state access (block hashes, storage keys, codes) during execution + // avoiding the need to re-execute the block later. + let vm_db: DynVmDatabase = Box::new(StoreVmDatabase::new( + self.storage.clone(), + parent_header.clone(), + )?); + + let logger = Arc::new(DatabaseLogger::new(Arc::new(vm_db))); + + let vm = match self.options.r#type.clone() { + BlockchainType::L1 => { + Evm::new_from_db_for_l1(logger.clone(), Arc::new(NativeCrypto)) + } + BlockchainType::L2(l2_config) => Evm::new_from_db_for_l2( + logger.clone(), + *l2_config.fee_config.read().map_err(|_| { + EvmError::Custom("Fee config lock was poisoned".to_string()) + })?, + Arc::new(NativeCrypto), + ), + }; + (vm, Some(logger)) + } else { + let vm_db = StoreVmDatabase::new(self.storage.clone(), parent_header.clone())?; + let vm = self.new_evm(vm_db)?; + (vm, None) }; - (vm, Some(logger)) - } else { - let vm_db = StoreVmDatabase::new(self.storage.clone(), parent_header.clone())?; - let vm = self.new_evm(vm_db)?; - (vm, None) - }; let ( res, diff --git a/crates/networking/rpc/engine/mod.rs b/crates/networking/rpc/engine/mod.rs index e330d4ab943..66944c003a7 100644 --- a/crates/networking/rpc/engine/mod.rs +++ b/crates/networking/rpc/engine/mod.rs @@ -3,11 +3,11 @@ pub mod client_version; pub mod exchange_transition_config; pub mod fork_choice; pub mod payload; -pub mod rest; #[cfg(feature = "eip-8025")] pub mod proof; #[cfg(feature = "eip-8025")] pub mod proof_types; +pub mod rest; use crate::{ rpc::{RpcApiContext, RpcHandler}, @@ -48,9 +48,7 @@ pub const CAPABILITIES: [&str; 24] = [ ]; /// REST capabilities advertised via `engine_exchangeCapabilities`. -pub const REST_CAPABILITIES: [&str; 1] = [ - "rest_engine_newPayloadWithWitness", -]; +pub const REST_CAPABILITIES: [&str; 1] = ["rest_engine_newPayloadWithWitness"]; /// EIP-8025 proof capabilities, advertised only when the feature is enabled. #[cfg(feature = "eip-8025")] diff --git a/crates/networking/rpc/engine/payload.rs b/crates/networking/rpc/engine/payload.rs index 6c20ebc479e..d7559eded56 100644 --- a/crates/networking/rpc/engine/payload.rs +++ b/crates/networking/rpc/engine/payload.rs @@ -890,7 +890,7 @@ fn validate_execution_payload_v2(payload: &ExecutionPayload) -> Result<(), RpcEr Ok(()) } -fn validate_execution_payload_v3(payload: &ExecutionPayload) -> Result<(), RpcErr> { +pub fn validate_execution_payload_v3(payload: &ExecutionPayload) -> Result<(), RpcErr> { // Validate that only the required arguments are present if payload.withdrawals.is_none() { return Err(RpcErr::WrongParam("withdrawals".to_string())); @@ -966,6 +966,44 @@ async fn validate_ancestors( Ok(None) } +// This function is used to make sure neither the current block nor its parent have been invalidated +// similar to validate_ancestors but returns PayloadStatusWithWitness +async fn validate_ancestors_with_witness( + block: &Block, + context: &RpcApiContext, +) -> Result, RpcErr> { + // Check if the block has already been invalidated + if let Some(latest_valid_hash) = context + .storage + .get_latest_valid_ancestor(block.hash()) + .await? + { + return Ok(Some(PayloadStatusWithWitness::invalid_with( + latest_valid_hash, + "Header has been previously invalidated.".into(), + ))); + } + + // Check if the parent block has already been invalidated + if let Some(latest_valid_hash) = context + .storage + .get_latest_valid_ancestor(block.header.parent_hash) + .await? + { + // Invalidate child too + context + .storage + .set_latest_valid_ancestor(block.header.hash(), latest_valid_hash) + .await?; + return Ok(Some(PayloadStatusWithWitness::invalid_with( + latest_valid_hash, + "Parent header has been previously invalidated.".into(), + ))); + } + + Ok(None) +} + async fn handle_new_payload_v1_v2( payload: &ExecutionPayload, block: Block, @@ -1000,7 +1038,42 @@ async fn handle_new_payload_v1_v2( Ok(payload_status) } -async fn handle_new_payload_v3( +async fn handle_new_payload_v1_v2_with_witness( + payload: &ExecutionPayload, + block: Block, + context: RpcApiContext, + bal: Option, +) -> Result { + let Some(syncer) = &context.syncer else { + return Err(RpcErr::Internal( + "New payload requested but syncer is not initialized".to_string(), + )); + }; + // Validate block hash + if let Err(RpcErr::Internal(error_msg)) = validate_block_hash(payload, &block) { + return Ok(PayloadStatusWithWitness::invalid_with_err(&error_msg)); + } + + // Check for invalid ancestors + if let Some(status) = validate_ancestors_with_witness(&block, &context).await? { + return Ok(status); + } + + // We have validated ancestors, the parent is correct + let latest_valid_hash = block.header.parent_hash; + + if syncer.sync_mode() == SyncMode::Snap { + debug!("Snap sync in progress, skipping new payload validation"); + return Ok(PayloadStatusWithWitness::syncing()); + } + + // All checks passed, execute payload + let payload_status = + try_execute_payload_with_witness(block, &context, latest_valid_hash, bal).await?; + Ok(payload_status) +} + +pub async fn handle_new_payload_v3( payload: &ExecutionPayload, context: RpcApiContext, block: Block, @@ -1024,6 +1097,30 @@ async fn handle_new_payload_v3( handle_new_payload_v1_v2(payload, block, context, bal).await } +pub async fn handle_new_payload_v3_with_witness( + payload: &ExecutionPayload, + context: RpcApiContext, + block: Block, + expected_blob_versioned_hashes: Vec, + bal: Option, +) -> Result { + // V3 specific: validate blob hashes + let blob_versioned_hashes: Vec = block + .body + .transactions + .iter() + .flat_map(|tx| tx.blob_versioned_hashes()) + .collect(); + + if expected_blob_versioned_hashes != blob_versioned_hashes { + return Ok(PayloadStatusWithWitness::invalid_with_err( + "Invalid blob_versioned_hashes", + )); + } + + handle_new_payload_v1_v2_with_witness(payload, block, context, bal).await +} + async fn handle_new_payload_v4( payload: &ExecutionPayload, context: RpcApiContext, @@ -1041,7 +1138,7 @@ async fn handle_new_payload_v4( // Elements of the list MUST be ordered by request_type in ascending order. // Elements with empty request_data MUST be excluded from the list. -fn validate_execution_requests(execution_requests: &[EncodedRequests]) -> Result<(), RpcErr> { +pub fn validate_execution_requests(execution_requests: &[EncodedRequests]) -> Result<(), RpcErr> { let mut last_type: i32 = -1; for requests in execution_requests { if requests.0.len() < 2 { @@ -1056,7 +1153,7 @@ fn validate_execution_requests(execution_requests: &[EncodedRequests]) -> Result Ok(()) } -fn get_block_from_payload( +pub fn get_block_from_payload( payload: &ExecutionPayload, parent_beacon_block_root: Option, requests_hash: Option, @@ -1178,6 +1275,89 @@ async fn try_execute_payload( } } +async fn try_execute_payload_with_witness( + block: Block, + context: &RpcApiContext, + latest_valid_hash: H256, + bal: Option, +) -> Result { + let Some(syncer) = &context.syncer else { + return Err(RpcErr::Internal( + "New payload requested but syncer is not initialized".to_string(), + )); + }; + let block_hash = block.hash(); + let block_number = block.header.number; + let storage = &context.storage; + // Return the valid message directly if we already have it. + // We check for header only as we do not download the block bodies before the pivot during snap sync + // https://github.com/lambdaclass/ethrex/issues/1766 + if storage.get_block_header_by_hash(block_hash)?.is_some() { + return Ok(PayloadStatusWithWitness::valid_with_hash(block_hash)); + } + + // Execute and store the block + debug!(%block_hash, %block_number, "Executing payload"); + + match add_block_with_witness(context, block, bal).await { + Err(ChainError::ParentNotFound) => { + // Start sync + syncer.sync_to_head(block_hash); + Ok(PayloadStatusWithWitness::syncing()) + } + // Under the current implementation this is not possible: we always calculate the state + // transition of any new payload as long as the parent is present. If we received the + // parent payload but it was stashed, then new payload would stash this one too, with a + // ParentNotFoundError. + Err(ChainError::ParentStateNotFound) => { + let e = "Failed to obtain parent state"; + error!("{e} for block {block_hash}"); + Err(RpcErr::Internal(e.to_string())) + } + Err(ChainError::InvalidBlock(error)) => { + warn!("Error executing block: {error}"); + context + .storage + .set_latest_valid_ancestor(block_hash, latest_valid_hash) + .await?; + Ok(PayloadStatusWithWitness::invalid_with( + latest_valid_hash, + error.to_string(), + )) + } + Err(ChainError::EvmError(error)) => { + warn!("Error executing block: {error}"); + context + .storage + .set_latest_valid_ancestor(block_hash, latest_valid_hash) + .await?; + Ok(PayloadStatusWithWitness::invalid_with( + latest_valid_hash, + error.to_string(), + )) + } + Err(ChainError::StoreError(error)) => { + warn!("Error storing block: {error}"); + Err(RpcErr::Internal(error.to_string())) + } + Err(e) => { + error!("{e} for block {block_hash}"); + Err(RpcErr::Internal(e.to_string())) + } + Ok(witness) => { + debug!("Block with hash {block_hash} executed and added to storage successfully"); + let Some(witness) = witness else { + debug!("No witness found for block {block_hash}"); + return Ok(PayloadStatusWithWitness::valid_with_hash(block_hash)); + }; + + Ok(PayloadStatusWithWitness::valid_with_witness_and_hash( + witness, block_hash, + )) + } + } +} + fn parse_get_payload_request(params: &Option>) -> Result { let params = params .as_ref() @@ -1255,9 +1435,8 @@ async fn get_payload(payload_id: u64, context: &RpcApiContext) -> Result, } -/// Public wrapper around `validate_execution_payload_v4` for use by the REST module. -#[inline] -pub fn validate_execution_payload_v4_public(payload: &ExecutionPayload) -> Result<(), RpcErr> { - validate_execution_payload_v4(payload) -} - -/// Public wrapper around `validate_execution_payload_v3` for use by the REST module. -#[inline] -pub fn validate_execution_payload_v3_public(payload: &ExecutionPayload) -> Result<(), RpcErr> { - validate_execution_payload_v3(payload) -} - -/// Public wrapper around `validate_execution_requests` for use by the REST module. -#[inline] -pub fn validate_execution_requests_public( - execution_requests: &[EncodedRequests], -) -> Result<(), RpcErr> { - validate_execution_requests(execution_requests) -} - -/// Channel message type for the witness block executor. -pub type WitnessBlockWorkerMessage = ( - oneshot::Sender, ChainError>>, - Block, - Option, -); +impl PayloadStatusWithWitness { + // Convenience methods to create payload status -/// Submit a block for execution + witness generation via the witness worker. -pub async fn add_block_with_witness( - ctx: &RpcApiContext, - block: Block, - bal: Option, -) -> Result { - let Some(syncer) = &ctx.syncer else { - return Err(RpcErr::Internal( - "New payload requested but syncer is not initialized".to_string(), - )); - }; + pub fn invalid_with(latest_valid_hash: H256, error: String) -> Self { + PayloadStatusWithWitness { + status: PayloadValidationStatus::Invalid, + latest_valid_hash: Some(latest_valid_hash), + validation_error: Some(error), + witness: None, + } + } - let block_hash = block.hash(); - let block_number = block.header.number; - let storage = &ctx.storage; + /// Creates a PayloadStatus with invalid status and error message + pub fn invalid_with_err(error: &str) -> Self { + PayloadStatusWithWitness { + status: PayloadValidationStatus::Invalid, + latest_valid_hash: None, + validation_error: Some(error.to_string()), + witness: None, + } + } - // Return VALID immediately if we already have this block. - if storage.get_block_header_by_hash(block_hash)?.is_some() { - return Ok(PayloadStatusWithWitness { - status: PayloadValidationStatus::Valid, - latest_valid_hash: Some(block_hash), + /// Creates a PayloadStatus with invalid status and latest valid hash + pub fn invalid_with_hash(hash: BlockHash) -> Self { + PayloadStatusWithWitness { + status: PayloadValidationStatus::Invalid, + latest_valid_hash: Some(hash), validation_error: None, witness: None, - }); + } } - // Check for previously invalidated ancestors. - if let Some(status) = validate_ancestors(&block, ctx).await? { - return Ok(PayloadStatusWithWitness { - status: status.status, - latest_valid_hash: status.latest_valid_hash, - validation_error: status.validation_error, + /// Creates a PayloadStatus with syncing status and no other info + pub fn syncing() -> Self { + PayloadStatusWithWitness { + status: PayloadValidationStatus::Syncing, + latest_valid_hash: None, + validation_error: None, witness: None, - }); + } } - let latest_valid_hash = block.header.parent_hash; - - if syncer.sync_mode() == ethrex_p2p::sync::SyncMode::Snap { - debug!("Snap sync in progress, skipping new payload validation"); - return Ok(PayloadStatusWithWitness { - status: PayloadValidationStatus::Syncing, + /// Creates a PayloadStatus with valid status and latest valid hash + pub fn valid_with_hash(hash: BlockHash) -> Self { + PayloadStatusWithWitness { + status: PayloadValidationStatus::Valid, + latest_valid_hash: Some(hash), + validation_error: None, + witness: None, + } + } + /// Creates a PayloadStatus with valid status and latest valid hash + pub fn valid() -> Self { + PayloadStatusWithWitness { + status: PayloadValidationStatus::Valid, latest_valid_hash: None, validation_error: None, witness: None, - }); + } } - // Execute via the witness worker channel. - debug!(%block_hash, %block_number, "Executing payload with witness"); + pub fn valid_with_witness_and_hash(witness: RpcExecutionWitness, hash: BlockHash) -> Self { + PayloadStatusWithWitness { + status: PayloadValidationStatus::Valid, + latest_valid_hash: Some(hash), + validation_error: None, + witness: Some(witness), + } + } +} + +/// Channel message type for the witness block executor. +pub type WitnessBlockWorkerMessage = ( + oneshot::Sender, ChainError>>, + Block, + Option, +); +/// Submit a block for execution + witness generation via the witness worker. +pub async fn add_block_with_witness( + ctx: &RpcApiContext, + block: Block, + bal: Option, +) -> Result, ChainError> { let (notify_send, notify_recv) = oneshot::channel(); ctx.witness_block_worker_channel .as_ref() - .ok_or_else(|| RpcErr::Internal("witness block worker not available".to_string()))? + .ok_or_else(|| ChainError::Custom("witness block worker not available".to_string()))? .send((notify_send, block, bal)) .map_err(|e| { - RpcErr::Internal(format!( - "failed to send witness block execution request to worker: {e}" + ChainError::Custom(format!( + "failed to send block execution request to worker: {e}" )) })?; - - let exec_result = notify_recv + notify_recv .await - .map_err(|e| { - RpcErr::Internal(format!( - "failed to receive witness block execution result: {e}" - )) - })?; - - match exec_result { - Err(ChainError::ParentNotFound) => { - syncer.sync_to_head(block_hash); - Ok(PayloadStatusWithWitness { - status: PayloadValidationStatus::Syncing, - latest_valid_hash: None, - validation_error: None, - witness: None, - }) - } - Err(ChainError::ParentStateNotFound) => { - let e = "Failed to obtain parent state"; - error!("{e} for block {block_hash}"); - Err(RpcErr::Internal(e.to_string())) - } - Err(ChainError::InvalidBlock(error)) => { - warn!("Error executing block: {error}"); - ctx.storage - .set_latest_valid_ancestor(block_hash, latest_valid_hash) - .await?; - Ok(PayloadStatusWithWitness { - status: PayloadValidationStatus::Invalid, - latest_valid_hash: Some(latest_valid_hash), - validation_error: Some(error.to_string()), - witness: None, - }) - } - Err(ChainError::EvmError(error)) => { - warn!("Error executing block: {error}"); - ctx.storage - .set_latest_valid_ancestor(block_hash, latest_valid_hash) - .await?; - Ok(PayloadStatusWithWitness { - status: PayloadValidationStatus::Invalid, - latest_valid_hash: Some(latest_valid_hash), - validation_error: Some(error.to_string()), - witness: None, - }) - } - Err(ChainError::StoreError(error)) => { - warn!("Error storing block: {error}"); - Err(RpcErr::Internal(error.to_string())) - } - Err(e) => { - error!("{e} for block {block_hash}"); - Err(RpcErr::Internal(e.to_string())) - } - Ok(witness) => { - debug!("Block with hash {block_hash} executed and added to storage successfully"); - Ok(PayloadStatusWithWitness { - status: PayloadValidationStatus::Valid, - latest_valid_hash: Some(block_hash), - validation_error: None, - witness, - }) - } - } + .map_err(|e| ChainError::Custom(format!("failed to receive block execution result: {e}")))? } - diff --git a/crates/networking/rpc/engine/rest.rs b/crates/networking/rpc/engine/rest.rs index 98094a2b897..29c88554598 100644 --- a/crates/networking/rpc/engine/rest.rs +++ b/crates/networking/rpc/engine/rest.rs @@ -22,14 +22,16 @@ use ethrex_common::H256; use ethrex_common::types::block_execution_witness::RpcExecutionWitness; use ethrex_common::types::requests::{EncodedRequests, compute_requests_hash}; -use crate::authentication::authenticate; -use crate::rpc::RpcApiContext; use crate::types::payload::{ExecutionPayload, PayloadValidationStatus}; use crate::utils::RpcErr; +use crate::{authentication::authenticate, engine::payload::get_block_from_payload}; +use crate::{ + engine::payload::{handle_new_payload_v3_with_witness, validate_execution_payload_v3}, + rpc::RpcApiContext, +}; use super::payload; - /// `Union[None, ByteVector[32]]` for `latest_valid_hash`. #[derive(SszEncode)] #[ssz(enum_behaviour = "union")] @@ -81,7 +83,6 @@ pub struct SszExecutionWitnessV1 { pub headers: Vec>, } - impl SszNewPayloadWithWitnessResponse { /// Build from a validation status + optional witness. fn from_status( @@ -168,20 +169,18 @@ fn rpc_err_to_response(err: RpcErr) -> axum::response::Response { RpcErr::BadParams(msg) | RpcErr::WrongParam(msg) | RpcErr::InvalidPayload(msg) => { json_error_response(-32602, msg) } - RpcErr::UnsupportedFork(msg) => { - json_error_response(-38005, msg) - } + RpcErr::UnsupportedFork(msg) => json_error_response(-38005, msg), RpcErr::Internal(msg) => json_error_response(-32603, msg), _ => json_error_response(-32603, &format!("{err:?}")), } } - /// `POST /new-payload-with-witness` /// /// Accepts JSON body: `[executionPayload, expectedBlobVersionedHashes, parentBeaconBlockRoot, executionRequests]` /// Returns SSZ-encoded `NewPayloadWithWitnessResponseV1`. pub async fn handle_new_payload_with_witness( + //TODO:DEVELOPERUCHE change to v4 State(context): State, auth_header: Option>>, body: axum::body::Bytes, @@ -198,10 +197,7 @@ pub async fn handle_new_payload_with_witness( }; if params.len() != 4 { - return json_error_response( - -32602, - &format!("Expected 4 params, got {}", params.len()), - ); + return json_error_response(-32602, &format!("Expected 4 params, got {}", params.len())); } let exec_payload: ExecutionPayload = match serde_json::from_value(params[0].clone()) { @@ -226,115 +222,57 @@ pub async fn handle_new_payload_with_witness( } }; - let execution_requests: Vec = match serde_json::from_value(params[3].clone()) - { + let execution_requests: Vec = match serde_json::from_value(params[3].clone()) { Ok(r) => r, Err(_) => { return json_error_response(-32602, "Invalid executionRequests"); } }; - // Use V4 validation when blockAccessList is present (Amsterdam+), V3 otherwise (Prague/Electra). - if exec_payload.block_access_list.is_some() { - if let Err(e) = payload::validate_execution_payload_v4_public(&exec_payload) { - return rpc_err_to_response(e); - } - } else { - if let Err(e) = payload::validate_execution_payload_v3_public(&exec_payload) { - return rpc_err_to_response(e); - } - } - - if let Err(e) = payload::validate_execution_requests_public(&execution_requests) { + if let Err(e) = payload::validate_execution_requests(&execution_requests) { return rpc_err_to_response(e); } let requests_hash = compute_requests_hash(&execution_requests); - let raw_bal_hash = params[0] - .get("blockAccessList") - .and_then(|v| v.as_str()) - .and_then(|hex_str| hex::decode(hex_str.trim_start_matches("0x")).ok()) - .map(|bytes| ethrex_common::utils::keccak(bytes)); - - let block = match exec_payload.clone().into_block( + let block = match get_block_from_payload( + &exec_payload, Some(parent_beacon_block_root), Some(requests_hash), - raw_bal_hash, + None, ) { - Ok(b) => b, - Err(e) => { - // Invalid block construction → return SSZ response with INVALID status. + Ok(block) => block, + Err(err) => { let resp = SszNewPayloadWithWitnessResponse::from_status( PayloadValidationStatus::Invalid, None, - Some(e.to_string()), + Some(err.to_string()), None, ); return ssz_response(resp); } }; - // TODO:DEVELOPERUCHE this config for a* should come externally - // if !chain_config.is_amsterdam_activated(block.header.timestamp) { - // return rpc_err_to_response(RpcErr::UnsupportedFork(format!( - // "{:?}", - // chain_config.get_fork(block.header.timestamp) - // ))); - // } let chain_config = context.storage.get_chain_config(); + if !chain_config.is_prague_activated(block.header.timestamp) { return rpc_err_to_response(RpcErr::UnsupportedFork(format!( "{:?}", chain_config.get_fork(block.header.timestamp) ))); } - - let bal = exec_payload.block_access_list.clone(); - if let Some(ref b) = bal { - if let Err(err) = b.validate_ordering() { - let resp = SszNewPayloadWithWitnessResponse::from_status( - PayloadValidationStatus::Invalid, - None, - Some(err), - None, - ); - return ssz_response(resp); - } - } - - let block_hash = exec_payload.block_hash; - let actual_block_hash = block.hash(); - if block_hash != actual_block_hash { - let resp = SszNewPayloadWithWitnessResponse::from_status( - PayloadValidationStatus::Invalid, - None, - Some(format!( - "Invalid block hash. Expected {actual_block_hash:#x}, got {block_hash:#x}" - )), - None, - ); - return ssz_response(resp); - } - - let blob_versioned_hashes: Vec = block - .body - .transactions - .iter() - .flat_map(|tx| tx.blob_versioned_hashes()) - .collect(); - - if expected_blob_versioned_hashes != blob_versioned_hashes { - let resp = SszNewPayloadWithWitnessResponse::from_status( - PayloadValidationStatus::Invalid, - None, - Some("Invalid blob_versioned_hashes".to_string()), - None, - ); - return ssz_response(resp); + // We use v3 since the execution payload remains the same. + if let Err(e) = validate_execution_payload_v3(&exec_payload) { + return rpc_err_to_response(e); } - - let payload_result = payload::add_block_with_witness(&context, block, bal).await; + let payload_result = handle_new_payload_v3_with_witness( + &exec_payload, + context, + block, + expected_blob_versioned_hashes.clone(), + None, + ) + .await; match payload_result { Ok(payload_status) => { diff --git a/crates/networking/rpc/rpc.rs b/crates/networking/rpc/rpc.rs index 6559649b682..34f219eb8ae 100644 --- a/crates/networking/rpc/rpc.rs +++ b/crates/networking/rpc/rpc.rs @@ -5,12 +5,14 @@ use crate::debug::execution_witness::ExecutionWitnessRequest; use crate::debug::execution_witness_by_hash::ExecutionWitnessByBlockHashRequest; use crate::engine::blobs::{BlobsV2Request, BlobsV3Request}; use crate::engine::client_version::GetClientVersionV1Request; -use crate::engine::payload::{GetPayloadV5Request, GetPayloadV6Request, NewPayloadV5Request, WitnessBlockWorkerMessage}; -use crate::engine::rest::handle_new_payload_with_witness; +use crate::engine::payload::{ + GetPayloadV5Request, GetPayloadV6Request, NewPayloadV5Request, WitnessBlockWorkerMessage, +}; #[cfg(feature = "eip-8025")] use crate::engine::proof::{ RequestProofsV1, VerifyExecutionProofV1, VerifyNewPayloadRequestHeaderV1, }; +use crate::engine::rest::handle_new_payload_with_witness; use crate::engine::{ ExchangeCapabilitiesRequest, blobs::BlobsV1Request, @@ -480,10 +482,7 @@ pub fn start_witness_block_executor( .spawn(move || { while let Some((notify, block, bal)) = rx.blocking_recv() { let _ = notify - .send( - blockchain - .add_block_pipeline_with_witness(block, bal.as_ref()), - ) + .send(blockchain.add_block_pipeline_with_witness(block, bal.as_ref())) .inspect_err(|_| tracing::error!("failed to notify witness caller")); } }) From 3c0be76a456cd547c69a0ded5dcaca4c8106c8f7 Mon Sep 17 00:00:00 2001 From: developeruche Date: Mon, 6 Apr 2026 16:56:41 +0100 Subject: [PATCH 04/13] implemented new-payload-with-witness-v5 --- crates/networking/rpc/engine/payload.rs | 18 ++- crates/networking/rpc/engine/rest.rs | 148 ++++++++++++++++++++++-- crates/networking/rpc/rpc.rs | 6 +- 3 files changed, 160 insertions(+), 12 deletions(-) diff --git a/crates/networking/rpc/engine/payload.rs b/crates/networking/rpc/engine/payload.rs index d7559eded56..6a53910cbaa 100644 --- a/crates/networking/rpc/engine/payload.rs +++ b/crates/networking/rpc/engine/payload.rs @@ -906,7 +906,7 @@ pub fn validate_execution_payload_v3(payload: &ExecutionPayload) -> Result<(), R } #[inline] -fn validate_execution_payload_v4(payload: &ExecutionPayload) -> Result<(), RpcErr> { +pub fn validate_execution_payload_v4(payload: &ExecutionPayload) -> Result<(), RpcErr> { // This method follows the same specification as `engine_newPayloadV4` additionally // rejects payload without block access list @@ -1136,6 +1136,22 @@ async fn handle_new_payload_v4( handle_new_payload_v3(payload, context, block, expected_blob_versioned_hashes, bal).await } +pub async fn handle_new_payload_v4_with_witness( + payload: &ExecutionPayload, + context: RpcApiContext, + block: Block, + expected_blob_versioned_hashes: Vec, + bal: Option, +) -> Result { + if let Some(bal) = &bal + && let Err(err) = bal.validate_ordering() + { + return Ok(PayloadStatusWithWitness::invalid_with_err(&err)); + } + handle_new_payload_v3_with_witness(payload, context, block, expected_blob_versioned_hashes, bal) + .await +} + // Elements of the list MUST be ordered by request_type in ascending order. // Elements with empty request_data MUST be excluded from the list. pub fn validate_execution_requests(execution_requests: &[EncodedRequests]) -> Result<(), RpcErr> { diff --git a/crates/networking/rpc/engine/rest.rs b/crates/networking/rpc/engine/rest.rs index 29c88554598..af3b270ab87 100644 --- a/crates/networking/rpc/engine/rest.rs +++ b/crates/networking/rpc/engine/rest.rs @@ -22,13 +22,16 @@ use ethrex_common::H256; use ethrex_common::types::block_execution_witness::RpcExecutionWitness; use ethrex_common::types::requests::{EncodedRequests, compute_requests_hash}; -use crate::types::payload::{ExecutionPayload, PayloadValidationStatus}; use crate::utils::RpcErr; use crate::{authentication::authenticate, engine::payload::get_block_from_payload}; use crate::{ engine::payload::{handle_new_payload_v3_with_witness, validate_execution_payload_v3}, rpc::RpcApiContext, }; +use crate::{ + engine::payload::{handle_new_payload_v4_with_witness, validate_execution_payload_v4}, + types::payload::{ExecutionPayload, PayloadValidationStatus}, +}; use super::payload; @@ -175,12 +178,8 @@ fn rpc_err_to_response(err: RpcErr) -> axum::response::Response { } } -/// `POST /new-payload-with-witness` -/// -/// Accepts JSON body: `[executionPayload, expectedBlobVersionedHashes, parentBeaconBlockRoot, executionRequests]` -/// Returns SSZ-encoded `NewPayloadWithWitnessResponseV1`. -pub async fn handle_new_payload_with_witness( - //TODO:DEVELOPERUCHE change to v4 +/// `POST /new-payload-with-witness-v4` +pub async fn handle_new_payload_with_witness_v4( State(context): State, auth_header: Option>>, body: axum::body::Bytes, @@ -262,7 +261,7 @@ pub async fn handle_new_payload_with_witness( ))); } // We use v3 since the execution payload remains the same. - if let Err(e) = validate_execution_payload_v3(&exec_payload) { + if let Err(e) = validate_execution_payload_v4(&exec_payload) { return rpc_err_to_response(e); } let payload_result = handle_new_payload_v3_with_witness( @@ -288,6 +287,139 @@ pub async fn handle_new_payload_with_witness( } } +/// `POST /new-payload-with-witness-v5` +pub async fn handle_new_payload_with_witness_v5( + State(context): State, + auth_header: Option>>, + body: axum::body::Bytes, +) -> axum::response::Response { + if let Err(_err) = authenticate(&context.node_data.jwt_secret, auth_header) { + return json_error_response(-32700, "JWT authentication failed"); + } + + let params: Vec = match serde_json::from_slice(&body) { + Ok(v) => v, + Err(e) => { + return json_error_response(-32700, &format!("Parse error: {e}")); + } + }; + + if params.len() != 4 { + return json_error_response(-32602, &format!("Expected 4 params, got {}", params.len())); + } + + // Extract the raw BAL hash from the JSON payload before deserialization. + // We hash the raw RLP bytes as-received to preserve the exact encoding + // (including any ordering) for accurate block hash validation. + let Ok(raw_bal_hash) = params[0] + .get("blockAccessList") + .map(|v| { + let hex_str = v + .as_str() + .ok_or(RpcErr::WrongParam("blockAccessList".to_string()))?; + let bytes = hex::decode(hex_str.trim_start_matches("0x")) + .map_err(|_| RpcErr::WrongParam("blockAccessList".to_string()))?; + Ok::<_, RpcErr>(ethrex_common::utils::keccak(bytes)) + }) + .transpose() + else { + return json_error_response(-32602, "Invalid blockAccessList"); + }; + + let exec_payload: ExecutionPayload = match serde_json::from_value(params[0].clone()) { + Ok(p) => p, + Err(_) => { + return json_error_response(-32602, "Invalid executionPayload"); + } + }; + + let expected_blob_versioned_hashes: Vec = match serde_json::from_value(params[1].clone()) + { + Ok(h) => h, + Err(_) => { + return json_error_response(-32602, "Invalid expectedBlobVersionedHashes"); + } + }; + + let parent_beacon_block_root: H256 = match serde_json::from_value(params[2].clone()) { + Ok(r) => r, + Err(_) => { + return json_error_response(-32602, "Invalid parentBeaconBlockRoot"); + } + }; + + let execution_requests: Vec = match serde_json::from_value(params[3].clone()) { + Ok(r) => r, + Err(_) => { + return json_error_response(-32602, "Invalid executionRequests"); + } + }; + + if let Err(e) = validate_execution_payload_v4(&exec_payload) { + return rpc_err_to_response(e); + } + + if let Err(e) = payload::validate_execution_requests(&execution_requests) { + return rpc_err_to_response(e); + } + + let requests_hash = compute_requests_hash(&execution_requests); + // Use the hash computed from the raw RLP bytes as-received. + // This preserves the exact encoding (including any ordering) from the payload, + // so the block hash check correctly detects BAL corruption. + let block_access_list_hash = raw_bal_hash; + + let block = match get_block_from_payload( + &exec_payload, + Some(parent_beacon_block_root), + Some(requests_hash), + block_access_list_hash, + ) { + Ok(block) => block, + Err(err) => { + let resp = SszNewPayloadWithWitnessResponse::from_status( + PayloadValidationStatus::Invalid, + None, + Some(err.to_string()), + None, + ); + return ssz_response(resp); + } + }; + + let chain_config = context.storage.get_chain_config(); + + if !chain_config.is_amsterdam_activated(block.header.timestamp) { + return rpc_err_to_response(RpcErr::UnsupportedFork(format!( + "{:?}", + chain_config.get_fork(block.header.timestamp) + ))); + } + + let bal = exec_payload.block_access_list.clone(); + let payload_result = handle_new_payload_v4_with_witness( + &exec_payload, + context, + block, + expected_blob_versioned_hashes.clone(), + bal, + ) + .await; + + match payload_result { + Ok(payload_status) => { + let resp = SszNewPayloadWithWitnessResponse::from_status( + payload_status.status, + payload_status.latest_valid_hash, + payload_status.validation_error, + payload_status.witness, + ); + ssz_response(resp) + } + Err(rpc_err) => rpc_err_to_response(rpc_err), + } +} + /// Produce a `200 OK` response with `Content-Type: application/octet-stream`. fn ssz_response(resp: SszNewPayloadWithWitnessResponse) -> axum::response::Response { let bytes = resp.to_ssz(); diff --git a/crates/networking/rpc/rpc.rs b/crates/networking/rpc/rpc.rs index 34f219eb8ae..73ae80f4071 100644 --- a/crates/networking/rpc/rpc.rs +++ b/crates/networking/rpc/rpc.rs @@ -12,7 +12,7 @@ use crate::engine::payload::{ use crate::engine::proof::{ RequestProofsV1, VerifyExecutionProofV1, VerifyNewPayloadRequestHeaderV1, }; -use crate::engine::rest::handle_new_payload_with_witness; +use crate::engine::rest::handle_new_payload_with_witness_v4; use crate::engine::{ ExchangeCapabilitiesRequest, blobs::BlobsV1Request, @@ -631,8 +631,8 @@ pub async fn start_api( let authrpc_router = Router::new() .route("/", post(authrpc_handler)) .route( - "/new-payload-with-witness", - post(handle_new_payload_with_witness), + "/new-payload-with-witness-v4", + post(handle_new_payload_with_witness_v4), ) .with_state(service_context.clone()) // Bump the body limit for the engine API to 256MB From 7119d7a85db36de31b9ad1a041d9f05a96fb2680 Mon Sep 17 00:00:00 2001 From: developeruche Date: Mon, 6 Apr 2026 20:15:42 +0100 Subject: [PATCH 05/13] clean-up --- crates/networking/rpc/engine/rest.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/networking/rpc/engine/rest.rs b/crates/networking/rpc/engine/rest.rs index af3b270ab87..42d6f12ca12 100644 --- a/crates/networking/rpc/engine/rest.rs +++ b/crates/networking/rpc/engine/rest.rs @@ -22,12 +22,9 @@ use ethrex_common::H256; use ethrex_common::types::block_execution_witness::RpcExecutionWitness; use ethrex_common::types::requests::{EncodedRequests, compute_requests_hash}; -use crate::utils::RpcErr; +use crate::{engine::payload::validate_execution_payload_v3, utils::RpcErr}; use crate::{authentication::authenticate, engine::payload::get_block_from_payload}; -use crate::{ - engine::payload::{handle_new_payload_v3_with_witness, validate_execution_payload_v3}, - rpc::RpcApiContext, -}; +use crate::{engine::payload::handle_new_payload_v3_with_witness, rpc::RpcApiContext}; use crate::{ engine::payload::{handle_new_payload_v4_with_witness, validate_execution_payload_v4}, types::payload::{ExecutionPayload, PayloadValidationStatus}, @@ -261,7 +258,7 @@ pub async fn handle_new_payload_with_witness_v4( ))); } // We use v3 since the execution payload remains the same. - if let Err(e) = validate_execution_payload_v4(&exec_payload) { + if let Err(e) = validate_execution_payload_v3(&exec_payload) { return rpc_err_to_response(e); } let payload_result = handle_new_payload_v3_with_witness( From 96a2f925f5429d6eef1736440f50f0034c0d03e1 Mon Sep 17 00:00:00 2001 From: developeruche Date: Tue, 7 Apr 2026 03:47:58 +0100 Subject: [PATCH 06/13] more clean-up and checkpoint for optimization --- crates/blockchain/blockchain.rs | 6 ++++-- crates/networking/rpc/engine/rest.rs | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/blockchain/blockchain.rs b/crates/blockchain/blockchain.rs index 618fb7744b1..06f22f963ac 100644 --- a/crates/blockchain/blockchain.rs +++ b/crates/blockchain/blockchain.rs @@ -1967,11 +1967,13 @@ impl Blockchain { if compute_witness { // Convert to RPC format for returning over the wire. + let block_hash = block.hash(); + self.storage + .store_witness(block_hash, block_number, witness.clone())?; + let rpc_witness = RpcExecutionWitness::try_from(witness) .map_err(|e| ChainError::Custom(format!("witness conversion failed: {e}")))?; produced_witness = Some(rpc_witness); - - //TODO:DEVELOPERUCHE: After seeing some data, Might persist to DB for later retrieval via debug_executionWitness. } else { // Persist to DB for later retrieval via debug_executionWitness. let block_hash = block.hash(); diff --git a/crates/networking/rpc/engine/rest.rs b/crates/networking/rpc/engine/rest.rs index 42d6f12ca12..aa689fdc167 100644 --- a/crates/networking/rpc/engine/rest.rs +++ b/crates/networking/rpc/engine/rest.rs @@ -22,9 +22,9 @@ use ethrex_common::H256; use ethrex_common::types::block_execution_witness::RpcExecutionWitness; use ethrex_common::types::requests::{EncodedRequests, compute_requests_hash}; -use crate::{engine::payload::validate_execution_payload_v3, utils::RpcErr}; use crate::{authentication::authenticate, engine::payload::get_block_from_payload}; use crate::{engine::payload::handle_new_payload_v3_with_witness, rpc::RpcApiContext}; +use crate::{engine::payload::validate_execution_payload_v3, utils::RpcErr}; use crate::{ engine::payload::{handle_new_payload_v4_with_witness, validate_execution_payload_v4}, types::payload::{ExecutionPayload, PayloadValidationStatus}, @@ -420,6 +420,7 @@ pub async fn handle_new_payload_with_witness_v5( /// Produce a `200 OK` response with `Content-Type: application/octet-stream`. fn ssz_response(resp: SszNewPayloadWithWitnessResponse) -> axum::response::Response { let bytes = resp.to_ssz(); + println!("SSZ response length: {:?}", bytes.len()); Response::builder() .status(StatusCode::OK) .header(header::CONTENT_TYPE, "application/octet-stream") From c889b39a953868ad5c06df45a3e5c6d7dc40969a Mon Sep 17 00:00:00 2001 From: developeruche Date: Thu, 9 Apr 2026 16:10:02 +0100 Subject: [PATCH 07/13] params parsing optimization --- crates/networking/rpc/engine/rest.rs | 119 ++++++++++++--------------- 1 file changed, 53 insertions(+), 66 deletions(-) diff --git a/crates/networking/rpc/engine/rest.rs b/crates/networking/rpc/engine/rest.rs index aa689fdc167..cfab91c861b 100644 --- a/crates/networking/rpc/engine/rest.rs +++ b/crates/networking/rpc/engine/rest.rs @@ -14,6 +14,7 @@ use axum_extra::{ TypedHeader, headers::{Authorization, authorization::Bearer}, }; +use bytes::Bytes; use libssz::SszEncode; use libssz_derive::SszEncode; use serde_json::Value; @@ -192,37 +193,9 @@ pub async fn handle_new_payload_with_witness_v4( } }; - if params.len() != 4 { - return json_error_response(-32602, &format!("Expected 4 params, got {}", params.len())); - } - - let exec_payload: ExecutionPayload = match serde_json::from_value(params[0].clone()) { - Ok(p) => p, - Err(_) => { - return json_error_response(-32602, "Invalid executionPayload"); - } - }; - - let expected_blob_versioned_hashes: Vec = match serde_json::from_value(params[1].clone()) - { - Ok(h) => h, - Err(_) => { - return json_error_response(-32602, "Invalid expectedBlobVersionedHashes"); - } - }; - - let parent_beacon_block_root: H256 = match serde_json::from_value(params[2].clone()) { - Ok(r) => r, - Err(_) => { - return json_error_response(-32602, "Invalid parentBeaconBlockRoot"); - } - }; - - let execution_requests: Vec = match serde_json::from_value(params[3].clone()) { - Ok(r) => r, - Err(_) => { - return json_error_response(-32602, "Invalid executionRequests"); - } + let (exec_payload, expected_blob_versioned_hashes, parent_beacon_block_root, execution_requests) = match parse_params(params) { + Ok(res) => res, + Err(e) => return json_error_response(-32602, &format!("Parse error: {e}")), }; if let Err(e) = payload::validate_execution_requests(&execution_requests) { @@ -265,7 +238,7 @@ pub async fn handle_new_payload_with_witness_v4( &exec_payload, context, block, - expected_blob_versioned_hashes.clone(), + expected_blob_versioned_hashes, None, ) .await; @@ -301,10 +274,6 @@ pub async fn handle_new_payload_with_witness_v5( } }; - if params.len() != 4 { - return json_error_response(-32602, &format!("Expected 4 params, got {}", params.len())); - } - // Extract the raw BAL hash from the JSON payload before deserialization. // We hash the raw RLP bytes as-received to preserve the exact encoding // (including any ordering) for accurate block hash validation. @@ -323,33 +292,9 @@ pub async fn handle_new_payload_with_witness_v5( return json_error_response(-32602, "Invalid blockAccessList"); }; - let exec_payload: ExecutionPayload = match serde_json::from_value(params[0].clone()) { - Ok(p) => p, - Err(_) => { - return json_error_response(-32602, "Invalid executionPayload"); - } - }; - - let expected_blob_versioned_hashes: Vec = match serde_json::from_value(params[1].clone()) - { - Ok(h) => h, - Err(_) => { - return json_error_response(-32602, "Invalid expectedBlobVersionedHashes"); - } - }; - - let parent_beacon_block_root: H256 = match serde_json::from_value(params[2].clone()) { - Ok(r) => r, - Err(_) => { - return json_error_response(-32602, "Invalid parentBeaconBlockRoot"); - } - }; - - let execution_requests: Vec = match serde_json::from_value(params[3].clone()) { - Ok(r) => r, - Err(_) => { - return json_error_response(-32602, "Invalid executionRequests"); - } + let (exec_payload, expected_blob_versioned_hashes, parent_beacon_block_root, execution_requests) = match parse_params(params) { + Ok(res) => res, + Err(e) => return json_error_response(-32602, &format!("Parse error: {e}")), }; if let Err(e) = validate_execution_payload_v4(&exec_payload) { @@ -398,7 +343,7 @@ pub async fn handle_new_payload_with_witness_v5( &exec_payload, context, block, - expected_blob_versioned_hashes.clone(), + expected_blob_versioned_hashes, bal, ) .await; @@ -419,8 +364,10 @@ pub async fn handle_new_payload_with_witness_v5( /// Produce a `200 OK` response with `Content-Type: application/octet-stream`. fn ssz_response(resp: SszNewPayloadWithWitnessResponse) -> axum::response::Response { - let bytes = resp.to_ssz(); - println!("SSZ response length: {:?}", bytes.len()); + let mut buf = Vec::with_capacity(resp.encoded_len()); + resp.ssz_append(&mut buf); + let bytes = Bytes::from(buf); + Response::builder() .status(StatusCode::OK) .header(header::CONTENT_TYPE, "application/octet-stream") @@ -428,3 +375,43 @@ fn ssz_response(resp: SszNewPayloadWithWitnessResponse) -> axum::response::Respo .expect("failed to build SSZ response") .into_response() } + +/// Parse the parameters for the new payload with witness RPC methods. +fn parse_params(params: Vec) -> Result<(ExecutionPayload, Vec, H256, Vec), String> { + if params.len() != 4 { + return Err(format!("Expected 4 params, got {}", params.len())); + } + + let exec_payload: ExecutionPayload = match serde_json::from_value(params[0].clone()) { + Ok(p) => p, + Err(_) => { + return Err(format!("Invalid executionPayload")); + } + }; + + let expected_blob_versioned_hashes: Vec = match serde_json::from_value(params[1].clone()) + { + Ok(h) => h, + Err(_) => { + return Err(format!("Invalid expectedBlobVersionedHashes")); + } + }; + + let parent_beacon_block_root: H256 = match serde_json::from_value(params[2].clone()) { + Ok(r) => r, + Err(_) => { + return Err(format!("Invalid parentBeaconBlockRoot")); + } + }; + + let execution_requests: Vec = match serde_json::from_value(params[3].clone()) { + Ok(r) => r, + Err(_) => { + return Err(format!("Invalid executionRequests")); + } + }; + + + Ok((exec_payload, expected_blob_versioned_hashes, parent_beacon_block_root, execution_requests)) +} + \ No newline at end of file From 8ea712eaa4d4c3e23440fe5fdf05443973db8b52 Mon Sep 17 00:00:00 2001 From: developeruche Date: Thu, 9 Apr 2026 17:35:48 +0100 Subject: [PATCH 08/13] implemented fire and forget functionality for witness and block store --- crates/blockchain/blockchain.rs | 195 +++++++++++++++++++++++++++++++- 1 file changed, 193 insertions(+), 2 deletions(-) diff --git a/crates/blockchain/blockchain.rs b/crates/blockchain/blockchain.rs index 06f22f963ac..7013d6a65f7 100644 --- a/crates/blockchain/blockchain.rs +++ b/crates/blockchain/blockchain.rs @@ -97,7 +97,7 @@ use payload::PayloadOrTask; use rustc_hash::{FxHashMap, FxHashSet}; use std::collections::hash_map::Entry; use std::collections::{BTreeMap, HashMap, HashSet}; -use std::sync::LazyLock; +use std::sync::{LazyLock, Mutex}; use std::sync::mpsc::Sender; use std::sync::{ Arc, RwLock, @@ -211,6 +211,11 @@ pub struct Blockchain { /// Persistent thread pool for merkleization workers. /// 17 threads: 16 shard workers + 1 watcher/coordination. merkle_pool: rayon::ThreadPool, + /// Lock to ensure the previous block's background store completes before the + /// next block's execution begins. Held by the background store thread during + /// `store_witness` + `store_block`, and acquired at the start of the next + /// `add_block_pipeline_inner` call. + store_lock: Arc>, } /// Configuration options for the blockchain. @@ -342,6 +347,7 @@ impl Blockchain { payloads: Arc::new(TokioMutex::new(Vec::new())), options: blockchain_opts, merkle_pool: Self::build_merkle_pool(), + store_lock: Arc::new(Mutex::new(())), } } @@ -353,6 +359,7 @@ impl Blockchain { payloads: Arc::new(TokioMutex::new(Vec::new())), options: BlockchainOptions::default(), merkle_pool: Self::build_merkle_pool(), + store_lock: Arc::new(Mutex::new(())), } } @@ -1862,7 +1869,7 @@ impl Blockchain { block: Block, bal: Option<&BlockAccessList>, ) -> Result, ChainError> { - let (_, result) = self.add_block_pipeline_inner(block, bal, true)?; + let (_, result) = self.add_block_pipeline_with_witness_inner(block, bal, true)?; result } @@ -1878,6 +1885,190 @@ impl Blockchain { Ok(produced_bal) } + /// A static version of `store_block` that can be used for background task and doesn't hold a reference to `self`. + fn store_block_static( + storage: &Store, + block: Block, + account_updates_list: AccountUpdatesList, + execution_result: BlockExecutionResult, + ) -> Result<(), ChainError> { + validate_state_root(&block.header, account_updates_list.state_trie_hash)?; + + let update_batch = UpdateBatch { + account_updates: account_updates_list.state_updates, + storage_updates: account_updates_list.storage_updates, + receipts: vec![(block.hash(), execution_result.receipts)], + blocks: vec![block], + code_updates: account_updates_list.code_updates, + batch_mode: false, + }; + + storage + .store_block_updates(update_batch) + .map_err(|e| e.into()) + } + + /// This is similar to `add_block_pipeline_inner` but optimized for witness generation. + fn add_block_pipeline_with_witness_inner( + &self, + block: Block, + bal: Option<&BlockAccessList>, + compute_witness: bool, + ) -> Result< + ( + Option, + Result, ChainError>, + ), + ChainError, + > { + // Wait for the previous block's background store to complete before + // starting execution. This ensures trie state is consistent. + let _prev_store = self.store_lock.lock().map_err(|_| { + ChainError::Custom("store_lock poisoned".to_string()) + })?; + drop(_prev_store); + + + // Validate if it can be the new head and find the parent + let Ok(parent_header) = find_parent_header(&block.header, &self.storage) else { + // If the parent is not present, we store it as pending. + self.storage.add_pending_block(block)?; + return Err(ChainError::ParentNotFound); + }; + + let (mut vm, logger) = + if (self.options.precompute_witnesses || compute_witness) && self.is_synced() { + // If witness pre-generation is enabled, we wrap the db with a logger + // to track state access (block hashes, storage keys, codes) during execution + // avoiding the need to re-execute the block later. + let vm_db: DynVmDatabase = Box::new(StoreVmDatabase::new( + self.storage.clone(), + parent_header.clone(), + )?); + + let logger = Arc::new(DatabaseLogger::new(Arc::new(vm_db))); + + let vm = match self.options.r#type.clone() { + BlockchainType::L1 => { + Evm::new_from_db_for_l1(logger.clone(), Arc::new(NativeCrypto)) + } + BlockchainType::L2(l2_config) => Evm::new_from_db_for_l2( + logger.clone(), + *l2_config.fee_config.read().map_err(|_| { + EvmError::Custom("Fee config lock was poisoned".to_string()) + })?, + Arc::new(NativeCrypto), + ), + }; + (vm, Some(logger)) + } else { + let vm_db = StoreVmDatabase::new(self.storage.clone(), parent_header.clone())?; + let vm = self.new_evm(vm_db)?; + (vm, None) + }; + + let ( + res, + account_updates_list, + accumulated_updates, + produced_bal, + merkle_queue_length, + instants, + warmer_duration, + ) = { self.execute_block_pipeline(&block, &parent_header, &mut vm, bal)? }; + + let (gas_used, gas_limit, block_number, transactions_count) = ( + block.header.gas_used, + block.header.gas_limit, + block.header.number, + block.body.transactions.len(), + ); + + let mut produced_witness: Option = None; + + if let Some(logger) = logger + && let Some(account_updates) = accumulated_updates + { + let witness = self.generate_witness_from_account_updates( + account_updates, + &block, + parent_header, + &logger, + )?; + + if compute_witness { + // Convert to RPC format for returning over the wire. + let block_hash = block.hash(); + self.storage + .store_witness(block_hash, block_number, witness.clone())?; + + produced_witness = Some(witness); + } else { + // Persist to DB for later retrieval via debug_executionWitness. + let block_hash = block.hash(); + self.storage + .store_witness(block_hash, block_number, witness)?; + } + }; + + // At this point the witness is ready to be returned, for the next set of actions, we would love to do a spawn and forget + // for - storing the witness + // for - storing the block (this is the most compute intensive part) + let store_lock = self.store_lock.clone(); + let produced_witness_clone = produced_witness.clone(); + let storage = self.storage.clone(); + let perf_logs_enabled = self.options.perf_logs_enabled; + + + std::thread::spawn(move || { + let _lock = store_lock.lock().unwrap_or_else(|e| e.into_inner()); + + if let Some(ref witness) = produced_witness_clone { + let block_hash = block.hash(); + if let Err(e) = storage.store_witness(block_hash, block_number, witness.clone()) { + ::tracing::warn!("Background store_witness failed: {e}"); + } + } + + if let Err(e) = Blockchain::store_block_static( + &storage, + block, + account_updates_list, + res, + ) { + ::tracing::error!("Background store_block failed: {e}"); + } + + let stored = Instant::now(); + + let instants = std::array::from_fn(move |i| { + if i < instants.len() { + instants[i] + } else { + stored + } + }); + + if perf_logs_enabled { + Self::print_add_block_pipeline_logs( + gas_used, + gas_limit, + block_number, + transactions_count, + merkle_queue_length, + warmer_duration, + instants, + ); + } + }); + + let witness = produced_witness.ok_or(ChainError::Custom("No witness produced".to_string()))?; + let rpc_witness = RpcExecutionWitness::try_from(witness) + .map_err(|e| ChainError::Custom(format!("witness conversion failed: {e}")))?; + + Ok((produced_bal, Ok(Some(rpc_witness)))) + } + /// Runs the full block pipeline (execute + merkleize + store). /// /// Returns a two-level Result: From aad90eb0e686f2722646004510317671d9ea03b1 Mon Sep 17 00:00:00 2001 From: developeruche Date: Thu, 9 Apr 2026 17:36:06 +0100 Subject: [PATCH 09/13] fmt --- crates/blockchain/blockchain.rs | 27 ++++++++++++--------------- crates/networking/rpc/engine/rest.rs | 27 +++++++++++++++++++++------ 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/crates/blockchain/blockchain.rs b/crates/blockchain/blockchain.rs index 7013d6a65f7..d1a4e2cb250 100644 --- a/crates/blockchain/blockchain.rs +++ b/crates/blockchain/blockchain.rs @@ -97,13 +97,13 @@ use payload::PayloadOrTask; use rustc_hash::{FxHashMap, FxHashSet}; use std::collections::hash_map::Entry; use std::collections::{BTreeMap, HashMap, HashSet}; -use std::sync::{LazyLock, Mutex}; use std::sync::mpsc::Sender; use std::sync::{ Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering}, mpsc::{Receiver, channel}, }; +use std::sync::{LazyLock, Mutex}; use std::time::{Duration, Instant}; use tokio::sync::Mutex as TokioMutex; use tokio_util::sync::CancellationToken; @@ -1923,12 +1923,12 @@ impl Blockchain { > { // Wait for the previous block's background store to complete before // starting execution. This ensures trie state is consistent. - let _prev_store = self.store_lock.lock().map_err(|_| { - ChainError::Custom("store_lock poisoned".to_string()) - })?; + let _prev_store = self + .store_lock + .lock() + .map_err(|_| ChainError::Custom("store_lock poisoned".to_string()))?; drop(_prev_store); - // Validate if it can be the new head and find the parent let Ok(parent_header) = find_parent_header(&block.header, &self.storage) else { // If the parent is not present, we store it as pending. @@ -2019,7 +2019,6 @@ impl Blockchain { let storage = self.storage.clone(); let perf_logs_enabled = self.options.perf_logs_enabled; - std::thread::spawn(move || { let _lock = store_lock.lock().unwrap_or_else(|e| e.into_inner()); @@ -2030,12 +2029,9 @@ impl Blockchain { } } - if let Err(e) = Blockchain::store_block_static( - &storage, - block, - account_updates_list, - res, - ) { + if let Err(e) = + Blockchain::store_block_static(&storage, block, account_updates_list, res) + { ::tracing::error!("Background store_block failed: {e}"); } @@ -2062,10 +2058,11 @@ impl Blockchain { } }); - let witness = produced_witness.ok_or(ChainError::Custom("No witness produced".to_string()))?; + let witness = + produced_witness.ok_or(ChainError::Custom("No witness produced".to_string()))?; let rpc_witness = RpcExecutionWitness::try_from(witness) - .map_err(|e| ChainError::Custom(format!("witness conversion failed: {e}")))?; - + .map_err(|e| ChainError::Custom(format!("witness conversion failed: {e}")))?; + Ok((produced_bal, Ok(Some(rpc_witness)))) } diff --git a/crates/networking/rpc/engine/rest.rs b/crates/networking/rpc/engine/rest.rs index cfab91c861b..84407afbad6 100644 --- a/crates/networking/rpc/engine/rest.rs +++ b/crates/networking/rpc/engine/rest.rs @@ -193,7 +193,12 @@ pub async fn handle_new_payload_with_witness_v4( } }; - let (exec_payload, expected_blob_versioned_hashes, parent_beacon_block_root, execution_requests) = match parse_params(params) { + let ( + exec_payload, + expected_blob_versioned_hashes, + parent_beacon_block_root, + execution_requests, + ) = match parse_params(params) { Ok(res) => res, Err(e) => return json_error_response(-32602, &format!("Parse error: {e}")), }; @@ -292,7 +297,12 @@ pub async fn handle_new_payload_with_witness_v5( return json_error_response(-32602, "Invalid blockAccessList"); }; - let (exec_payload, expected_blob_versioned_hashes, parent_beacon_block_root, execution_requests) = match parse_params(params) { + let ( + exec_payload, + expected_blob_versioned_hashes, + parent_beacon_block_root, + execution_requests, + ) = match parse_params(params) { Ok(res) => res, Err(e) => return json_error_response(-32602, &format!("Parse error: {e}")), }; @@ -377,7 +387,9 @@ fn ssz_response(resp: SszNewPayloadWithWitnessResponse) -> axum::response::Respo } /// Parse the parameters for the new payload with witness RPC methods. -fn parse_params(params: Vec) -> Result<(ExecutionPayload, Vec, H256, Vec), String> { +fn parse_params( + params: Vec, +) -> Result<(ExecutionPayload, Vec, H256, Vec), String> { if params.len() != 4 { return Err(format!("Expected 4 params, got {}", params.len())); } @@ -411,7 +423,10 @@ fn parse_params(params: Vec) -> Result<(ExecutionPayload, Vec, H256 } }; - - Ok((exec_payload, expected_blob_versioned_hashes, parent_beacon_block_root, execution_requests)) + Ok(( + exec_payload, + expected_blob_versioned_hashes, + parent_beacon_block_root, + execution_requests, + )) } - \ No newline at end of file From 51767b7c1cccdc64480f82a938b773d58376ec44 Mon Sep 17 00:00:00 2001 From: developeruche Date: Thu, 9 Apr 2026 23:41:54 +0100 Subject: [PATCH 10/13] fixed double storage bug --- crates/blockchain/blockchain.rs | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/crates/blockchain/blockchain.rs b/crates/blockchain/blockchain.rs index d1a4e2cb250..fbbfd10d1bf 100644 --- a/crates/blockchain/blockchain.rs +++ b/crates/blockchain/blockchain.rs @@ -1996,19 +1996,7 @@ impl Blockchain { &logger, )?; - if compute_witness { - // Convert to RPC format for returning over the wire. - let block_hash = block.hash(); - self.storage - .store_witness(block_hash, block_number, witness.clone())?; - - produced_witness = Some(witness); - } else { - // Persist to DB for later retrieval via debug_executionWitness. - let block_hash = block.hash(); - self.storage - .store_witness(block_hash, block_number, witness)?; - } + produced_witness = Some(witness); }; // At this point the witness is ready to be returned, for the next set of actions, we would love to do a spawn and forget From 746e8fe688471b192cb06d77a9fc163aebbeab8e Mon Sep 17 00:00:00 2001 From: developeruche Date: Sat, 23 May 2026 22:18:17 +0100 Subject: [PATCH 11/13] installed new-payload-with-witness ~v5 to the router --- crates/networking/rpc/rpc.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/networking/rpc/rpc.rs b/crates/networking/rpc/rpc.rs index 73ae80f4071..995fb11aa40 100644 --- a/crates/networking/rpc/rpc.rs +++ b/crates/networking/rpc/rpc.rs @@ -12,7 +12,7 @@ use crate::engine::payload::{ use crate::engine::proof::{ RequestProofsV1, VerifyExecutionProofV1, VerifyNewPayloadRequestHeaderV1, }; -use crate::engine::rest::handle_new_payload_with_witness_v4; +use crate::engine::rest::{handle_new_payload_with_witness_v4, handle_new_payload_with_witness_v5}; use crate::engine::{ ExchangeCapabilitiesRequest, blobs::BlobsV1Request, @@ -634,6 +634,10 @@ pub async fn start_api( "/new-payload-with-witness-v4", post(handle_new_payload_with_witness_v4), ) + .route( + "/new-payload-with-witness", + post(handle_new_payload_with_witness_v5), + ) .with_state(service_context.clone()) // Bump the body limit for the engine API to 256MB // This is needed to receive payloads bigger than the default limit of 2MB From 719828bc322521472d95982b3a125f4175d73d00 Mon Sep 17 00:00:00 2001 From: developeruche Date: Mon, 25 May 2026 16:52:44 +0100 Subject: [PATCH 12/13] update default kurtosis yaml --- fixtures/networks/default.yaml | 112 ++++----------------------------- 1 file changed, 11 insertions(+), 101 deletions(-) diff --git a/fixtures/networks/default.yaml b/fixtures/networks/default.yaml index 83433b89494..c3de8c46692 100644 --- a/fixtures/networks/default.yaml +++ b/fixtures/networks/default.yaml @@ -1,103 +1,3 @@ -# participants: -# # NOTE: Both erigon and geth work on this example, but they provide wrong nodes information on discovery protocol -# # - el_type: erigon -# # el_image: ethpandaops/erigon:main-764a2c50 -# # cl_type: lighthouse -# # cl_image: sigp/lighthouse:v8.0.0-rc.1 -# # validator_count: 32 -# # - el_type: reth -# # el_image: ghcr.io/paradigmxyz/reth:v1.2.2 -# # cl_type: lighthouse -# # cl_image: sigp/lighthouse:v8.0.0-rc.1 -# # validator_count: 32 -# - el_type: besu -# el_image: ethpandaops/besu:main-142a5e6 -# cl_type: lighthouse -# cl_image: sigp/lighthouse:v8.0.0-rc.1 -# validator_count: 32 -# - el_type: geth -# el_image: ethereum/client-go:v1.15.2 -# cl_type: lighthouse -# cl_image: sigp/lighthouse:v8.0.0-rc.1 -# validator_count: 32 -# count: 1 -# - el_type: ethrex -# el_image: ethrex:local -# cl_type: lighthouse -# cl_image: sigp/lighthouse:v8.0.0-rc.1 -# validator_count: 32 -# # snooper_enabled: true - -# ethereum_metrics_exporter_enabled: true - -# additional_services: -# - dora -# - spamoor -# - prometheus_grafana - -# spamoor_params: -# spammers: -# - scenario: erctx -# config: -# throughput: 750 - -# grafana_params: -# additional_dashboards: ["./ethrex_l1_perf.json"] - - - - - - - - -# participants: -# # NOTE: Both erigon and geth work on this example, but they provide wrong nodes information on discovery protocol -# # - el_type: erigon -# # el_image: ethpandaops/erigon:main-764a2c50 -# # cl_type: lighthouse -# # cl_image: sigp/lighthouse:v8.0.0-rc.1 -# # validator_count: 32 -# # - el_type: reth -# # el_image: ghcr.io/paradigmxyz/reth:v1.2.2 -# # cl_type: lighthouse -# # cl_image: sigp/lighthouse:v8.0.0-rc.1 -# # validator_count: 32 -# - el_type: besu -# el_image: ethpandaops/besu:main-142a5e6 -# cl_type: lighthouse -# cl_image: sigp/lighthouse:v8.0.0-rc.1 -# validator_count: 32 -# - el_type: geth -# el_image: ethereum/client-go:v1.15.2 -# cl_type: lighthouse -# cl_image: sigp/lighthouse:v8.0.0-rc.1 -# validator_count: 32 -# count: 1 -# - el_type: ethrex -# el_image: ethrex:local -# cl_type: lighthouse -# cl_image: sigp/lighthouse:v8.0.0-rc.1 -# validator_count: 32 -# # snooper_enabled: true - -# ethereum_metrics_exporter_enabled: true - -# additional_services: -# - dora -# - spamoor -# - prometheus_grafana - -# spamoor_params: -# spammers: -# - scenario: erctx -# config: -# throughput: 750 - -# grafana_params: -# additional_dashboards: ["./ethrex_l1_perf.json"] - - participants: # NOTE: Both erigon and geth work on this example, but they provide wrong nodes information on discovery protocol # - el_type: erigon @@ -139,4 +39,14 @@ ethereum_metrics_exporter_enabled: true additional_services: - dora - - prometheus_grafana \ No newline at end of file + - spamoor + - prometheus_grafana + +spamoor_params: + spammers: + - scenario: erctx + config: + throughput: 750 + +grafana_params: + additional_dashboards: ["./ethrex_l1_perf.json"] \ No newline at end of file From 5bf5aa77f42b9372de71211c82924514e216fde5 Mon Sep 17 00:00:00 2001 From: developeruche Date: Thu, 28 May 2026 06:18:29 +0100 Subject: [PATCH 13/13] clean debug todo --- crates/networking/rpc/engine/rest.rs | 5 +++++ crates/vm/levm/src/hooks/default_hook.rs | 16 ++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/crates/networking/rpc/engine/rest.rs b/crates/networking/rpc/engine/rest.rs index 84407afbad6..0057879a6bd 100644 --- a/crates/networking/rpc/engine/rest.rs +++ b/crates/networking/rpc/engine/rest.rs @@ -279,6 +279,11 @@ pub async fn handle_new_payload_with_witness_v5( } }; + // Guard against empty / short params before any direct indexing. + if params.is_empty() { + return json_error_response(-32602, "Parse error: Expected 4 params, got 0"); + } + // Extract the raw BAL hash from the JSON payload before deserialization. // We hash the raw RLP bytes as-received to preserve the exact encoding // (including any ordering) for accurate block hash validation. diff --git a/crates/vm/levm/src/hooks/default_hook.rs b/crates/vm/levm/src/hooks/default_hook.rs index 31979a2d3da..9f304519b84 100644 --- a/crates/vm/levm/src/hooks/default_hook.rs +++ b/crates/vm/levm/src/hooks/default_hook.rs @@ -82,14 +82,14 @@ impl Hook for DefaultHook { vm.increment_account_nonce(sender_address) .map_err(|_| TxValidationError::NonceIsMax)?; - // TODO:DEVELOPERUCHE check for nonce mismatch - // if sender_info.nonce != vm.env.tx_nonce { - // return Err(TxValidationError::NonceMismatch { - // expected: sender_info.nonce, - // actual: vm.env.tx_nonce, - // } - // .into()); - // } + // check for nonce mismatch + if sender_info.nonce != vm.env.tx_nonce { + return Err(TxValidationError::NonceMismatch { + expected: sender_info.nonce, + actual: vm.env.tx_nonce, + } + .into()); + } // (8) PRIORITY_GREATER_THAN_MAX_FEE_PER_GAS if let (Some(tx_max_priority_fee), Some(tx_max_fee_per_gas)) = (