From 459d8c6e77f0d8b67e6d20775a66f684eabe9a3e Mon Sep 17 00:00:00 2001 From: frisitano Date: Tue, 24 Mar 2026 17:58:34 +0100 Subject: [PATCH 1/2] proof engine tests --- Cargo.lock | 1 + .../src/eip8025/json_structures.rs | 134 ------------ .../src/eip8025/proof_engine.rs | 25 +-- .../execution_layer/src/eip8025/tests.rs | 38 ++-- .../src/engine_api/new_payload_request.rs | 3 +- beacon_node/execution_layer/src/lib.rs | 8 +- .../src/test_utils/mock_proof_node_client.rs | 191 +++++++++++------- .../execution_layer/src/test_utils/mod.rs | 2 +- .../lighthouse_network/src/discovery/enr.rs | 2 +- .../network/src/sync/network_context.rs | 8 +- testing/proof_engine/src/lib.rs | 23 ++- testing/simulator/Cargo.toml | 1 + testing/simulator/src/basic_sim.rs | 1 + testing/simulator/src/fallback_sim.rs | 1 + testing/simulator/src/local_network.rs | 51 +++-- testing/simulator/src/test_utils/builder.rs | 1 + validator_client/src/lib.rs | 6 +- 17 files changed, 210 insertions(+), 286 deletions(-) delete mode 100644 beacon_node/execution_layer/src/eip8025/json_structures.rs diff --git a/Cargo.lock b/Cargo.lock index 5762b01e921..6f8fceefdd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11103,6 +11103,7 @@ dependencies = [ "kzg 0.1.0", "lighthouse_network", "logging", + "network_utils", "node_test_rig", "parking_lot", "rayon", diff --git a/beacon_node/execution_layer/src/eip8025/json_structures.rs b/beacon_node/execution_layer/src/eip8025/json_structures.rs deleted file mode 100644 index ce638e09b91..00000000000 --- a/beacon_node/execution_layer/src/eip8025/json_structures.rs +++ /dev/null @@ -1,134 +0,0 @@ -//! JSON structures for EIP-8025 Engine API communication. -//! -//! These types are used for JSON-RPC serialization/deserialization with the execution engine. - -use crate::eip8025::ProofEngineError; -use serde::{Deserialize, Serialize}; -use strum::EnumString; -use types::execution::eip8025::{ProofData, ProofStatus}; -use types::{Hash256, ProofGenId}; - -// TODO: Consider if this type is necessary or if we can use existing ProofInput type. -/// JSON representation of PublicInput for Engine API. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct JsonPublicInputV1 { - /// The tree hash root of the NewPayloadRequest - pub new_payload_request_root: Hash256, -} - -/// JSON representation of ExecutionProof for Engine API. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct JsonExecutionProofV1 { - /// The proof data (hex encoded) - #[serde(with = "ssz_types::serde_utils::hex_var_list")] - pub proof_data: ProofData, - /// The type of proof - #[serde(with = "serde_utils::quoted_u64")] - pub proof_type: u64, - /// Public input linking the proof to a specific payload request - pub public_input: JsonPublicInputV1, -} - -/// JSON representation of ProofStatus for Engine API responses. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct JsonProofStatusV1 { - /// The status: "VALID", "INVALID", "ACCEPTED", or "NOT_SUPPORTED" - pub status: JsonProofStatusV1Status, - /// Optional error message - #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option, -} - -#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, EnumString)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -#[strum(serialize_all = "SCREAMING_SNAKE_CASE")] -pub enum JsonProofStatusV1Status { - Valid, - Invalid, - Accepted, - NotSupported, -} - -/// JSON representation of ProofAttributes for proof requests. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct JsonProofAttributesV1 { - /// List of proof types to generate - pub proof_types: Vec, -} - -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] -#[serde(transparent)] -pub struct TransparentJsonProofGenId(#[serde(with = "serde_utils::bytes_8_hex")] pub ProofGenId); - -impl From for ProofGenId { - fn from(json: TransparentJsonProofGenId) -> Self { - json.0 - } -} - -impl From for JsonPublicInputV1 { - fn from(input: types::execution::eip8025::PublicInput) -> Self { - JsonPublicInputV1 { - new_payload_request_root: input.new_payload_request_root, - } - } -} - -impl From for JsonExecutionProofV1 { - fn from(proof: types::execution::eip8025::ExecutionProof) -> Self { - JsonExecutionProofV1 { - proof_data: proof.proof_data, - proof_type: proof.proof_type as u64, - public_input: proof.public_input.into(), - } - } -} - -impl From for ProofStatus { - fn from(j: JsonProofStatusV1) -> Self { - // Use this verbose deconstruction pattern to ensure no field is left unused. - let JsonProofStatusV1 { status, .. } = j; - - status.into() - } -} - -impl From for ProofStatus { - fn from(status: JsonProofStatusV1Status) -> Self { - match status { - JsonProofStatusV1Status::Valid => ProofStatus::Valid, - JsonProofStatusV1Status::Invalid => ProofStatus::Invalid, - JsonProofStatusV1Status::Accepted => ProofStatus::Accepted, - JsonProofStatusV1Status::NotSupported => ProofStatus::NotSupported, - } - } -} - -impl From for JsonProofAttributesV1 { - fn from(attrs: types::execution::eip8025::ProofAttributes) -> Self { - JsonProofAttributesV1 { - proof_types: attrs.proof_types.into_iter().map(|t| t as u64).collect(), - } - } -} - -impl TryFrom for types::execution::eip8025::ProofAttributes { - type Error = ProofEngineError; - - fn try_from(json: JsonProofAttributesV1) -> Result { - Ok(types::execution::eip8025::ProofAttributes { - proof_types: json - .proof_types - .into_iter() - .map(|t| { - t.try_into() - .map_err(|_| ProofEngineError::InvalidProofType(t.to_string())) - }) - .collect::, _>>()?, - }) - } -} diff --git a/beacon_node/execution_layer/src/eip8025/proof_engine.rs b/beacon_node/execution_layer/src/eip8025/proof_engine.rs index c24e02c4fdf..9a63d0bb664 100644 --- a/beacon_node/execution_layer/src/eip8025/proof_engine.rs +++ b/beacon_node/execution_layer/src/eip8025/proof_engine.rs @@ -182,28 +182,9 @@ impl HttpProofEngine { new_payload_request: NewPayloadRequest<'_, E>, proof_attributes: ProofAttributes, ) -> Result { - match new_payload_request { - NewPayloadRequest::Bellatrix(_) => { - Err(ProofEngineError::ForkNotSupported("Bellatrix".to_string())) - } - NewPayloadRequest::Capella(_) => { - Err(ProofEngineError::ForkNotSupported("Capella".to_string())) - } - NewPayloadRequest::Deneb(_) => { - Err(ProofEngineError::ForkNotSupported("Deneb".to_string())) - } - NewPayloadRequest::Electra(_) => { - Err(ProofEngineError::ForkNotSupported("Electra".to_string())) - } - NewPayloadRequest::Fulu(fulu) => { - self.proof_node - .request_proofs(fulu.as_ssz_bytes(), proof_attributes) - .await - } - NewPayloadRequest::Gloas(_) => { - Err(ProofEngineError::ForkNotSupported("Gloas".to_string())) - } - } + self.proof_node + .request_proofs(new_payload_request.as_ssz_bytes(), proof_attributes) + .await } /// Snapshot the current state into a persisted form for serialization. diff --git a/beacon_node/execution_layer/src/eip8025/tests.rs b/beacon_node/execution_layer/src/eip8025/tests.rs index 882d33dea34..657f6ce7517 100644 --- a/beacon_node/execution_layer/src/eip8025/tests.rs +++ b/beacon_node/execution_layer/src/eip8025/tests.rs @@ -6,10 +6,10 @@ use crate::test_utils::{MockClientEvent, MockProofNodeClient, make_test_fulu_ssz use bls::{FixedBytesExtended, SignatureBytes}; use futures::StreamExt; use tokio::time::{Duration, timeout}; -use types::Hash256; use types::execution::eip8025::{ ExecutionProof, ProofAttributes, PublicInput, SignedExecutionProof, }; +use types::{Hash256, MainnetEthSpec}; // ─── helpers ───────────────────────────────────────────────────────────────── @@ -40,10 +40,10 @@ async fn next_event(rx: &mut tokio::sync::broadcast::Receiver) /// `request_proofs` decodes SSZ, records the body, and emits `ProofRequested`. #[tokio::test] async fn mock_client_request_proofs_emits_event() { - let mock = MockProofNodeClient::new(0); + let mock = MockProofNodeClient::::new(0); let mut rx = mock.subscribe_client_events(); - let (body, expected_root) = make_test_fulu_ssz(Hash256::repeat_byte(0xAA)); + let (body, expected_root) = make_test_fulu_ssz::(Hash256::repeat_byte(0xAA)); let attrs = ProofAttributes { proof_types: vec![1, 2], }; @@ -67,7 +67,7 @@ async fn mock_client_request_proofs_emits_event() { /// `verify_proof` emits `ProofVerified`. #[tokio::test] async fn mock_client_verify_proof_emits_event() { - let mock = MockProofNodeClient::new(0); + let mock = MockProofNodeClient::::new(0); let mut rx = mock.subscribe_client_events(); let root = Hash256::repeat_byte(0xBB); @@ -83,7 +83,7 @@ async fn mock_client_verify_proof_emits_event() { /// `get_proof` emits `ProofFetched`. #[tokio::test] async fn mock_client_get_proof_emits_event() { - let mock = MockProofNodeClient::new(0); + let mock = MockProofNodeClient::::new(0); let mut rx = mock.subscribe_client_events(); let root = Hash256::repeat_byte(0xCC); @@ -99,13 +99,13 @@ async fn mock_client_get_proof_emits_event() { /// `request_proofs` broadcasts a `ProofComplete` SSE event for each proof type. #[tokio::test] async fn mock_client_request_proofs_broadcasts_sse_events() { - let mock = MockProofNodeClient::new(0); + let mock = MockProofNodeClient::::new(0); let mut sse = mock.subscribe_proof_events(None); let attrs = ProofAttributes { proof_types: vec![0, 1], }; - let (body, expected_root) = make_test_fulu_ssz(Hash256::repeat_byte(0x42)); + let (body, expected_root) = make_test_fulu_ssz::(Hash256::repeat_byte(0x42)); let root = mock .request_proofs(body, attrs) .await @@ -127,11 +127,11 @@ async fn mock_client_request_proofs_broadcasts_sse_events() { /// Multiple subscribers each receive every event independently. #[tokio::test] async fn mock_client_multiple_subscribers_each_get_events() { - let mock = MockProofNodeClient::new(0); + let mock = MockProofNodeClient::::new(0); let mut rx1 = mock.subscribe_client_events(); let mut rx2 = mock.subscribe_client_events(); - let (body, _) = make_test_fulu_ssz(Hash256::repeat_byte(0x01)); + let (body, _) = make_test_fulu_ssz::(Hash256::repeat_byte(0x01)); let _ = mock .request_proofs( body, @@ -155,14 +155,14 @@ async fn mock_client_multiple_subscribers_each_get_events() { /// Different SSZ bodies produce different roots (computed via tree-hash). #[tokio::test] async fn mock_client_computes_distinct_roots_from_ssz() { - let mock = MockProofNodeClient::new(0); + let mock = MockProofNodeClient::::new(0); let attrs = ProofAttributes { proof_types: vec![], }; - let (body1, expected1) = make_test_fulu_ssz(Hash256::repeat_byte(0x01)); - let (body2, expected2) = make_test_fulu_ssz(Hash256::repeat_byte(0x02)); - let (body3, expected3) = make_test_fulu_ssz(Hash256::repeat_byte(0x03)); + let (body1, expected1) = make_test_fulu_ssz::(Hash256::repeat_byte(0x01)); + let (body2, expected2) = make_test_fulu_ssz::(Hash256::repeat_byte(0x02)); + let (body3, expected3) = make_test_fulu_ssz::(Hash256::repeat_byte(0x03)); let root1 = mock.request_proofs(body1, attrs.clone()).await.unwrap(); let root2 = mock.request_proofs(body2, attrs.clone()).await.unwrap(); @@ -182,7 +182,7 @@ async fn mock_client_computes_distinct_roots_from_ssz() { /// call `verify_proof` on the underlying client. #[tokio::test] async fn engine_verify_proof_unknown_root_returns_syncing() { - let mock = MockProofNodeClient::new(0); + let mock = MockProofNodeClient::::new(0); let mut rx = mock.subscribe_client_events(); let engine = HttpProofEngine::with_proof_node(mock); @@ -207,7 +207,7 @@ async fn engine_verify_proof_unknown_root_returns_syncing() { /// `get_proof` delegates to the underlying client and emits `ProofFetched`. #[tokio::test] async fn engine_get_proof_delegates_to_client() { - let mock = MockProofNodeClient::new(0); + let mock = MockProofNodeClient::::new(0); let mut rx = mock.subscribe_client_events(); let engine = HttpProofEngine::with_proof_node(mock); @@ -230,7 +230,7 @@ async fn engine_get_proof_delegates_to_client() { /// the buffer grows while no `ProofVerified` event is emitted. #[tokio::test] async fn engine_unknown_root_proof_is_buffered() { - let mock = MockProofNodeClient::new(0); + let mock = MockProofNodeClient::::new(0); let mut rx = mock.subscribe_client_events(); let engine = HttpProofEngine::with_proof_node(mock); @@ -254,13 +254,13 @@ async fn engine_unknown_root_proof_is_buffered() { /// `subscribe_proof_events` with a root filter only forwards matching events. #[tokio::test] async fn engine_subscribe_proof_events_filters_by_root() { - let mock = MockProofNodeClient::new(0); + let mock = MockProofNodeClient::::new(0); let attrs = ProofAttributes { proof_types: vec![0], }; - let (body1, root1) = make_test_fulu_ssz(Hash256::from_low_u64_be(1)); - let (body2, _root2) = make_test_fulu_ssz(Hash256::from_low_u64_be(2)); + let (body1, root1) = make_test_fulu_ssz::(Hash256::from_low_u64_be(1)); + let (body2, _root2) = make_test_fulu_ssz::(Hash256::from_low_u64_be(2)); // Subscribe before making requests. let mut filtered = mock.subscribe_proof_events(Some(root1)); diff --git a/beacon_node/execution_layer/src/engine_api/new_payload_request.rs b/beacon_node/execution_layer/src/engine_api/new_payload_request.rs index 6ef617a0bff..ff3d3a7260e 100644 --- a/beacon_node/execution_layer/src/engine_api/new_payload_request.rs +++ b/beacon_node/execution_layer/src/engine_api/new_payload_request.rs @@ -29,8 +29,9 @@ use types::{ expr = "BeaconStateError::IncorrectStateVariant" ) )] -#[derive(Clone, Debug, PartialEq, TreeHash)] +#[derive(Clone, Debug, PartialEq, SszEncode, TreeHash)] #[tree_hash(enum_behaviour = "transparent")] +#[ssz(enum_behaviour = "transparent")] pub struct NewPayloadRequest<'block, E: EthSpec> { #[superstruct( only(Bellatrix), diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index 657d6ffe5ea..fa8916639ae 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -580,17 +580,15 @@ impl ExecutionLayer { let proof_engine: Option> = if let Some(proof_url) = proof_engine_endpoint { if let Some(idx) = test_utils::parse_mock_index(proof_url.expose_full().as_str()) { - let mock = test_utils::get_mock_proof_engine(idx).unwrap_or_else(|| { + let mock = test_utils::get_mock_proof_engine::(idx).unwrap_or_else(|| { debug!( idx, "No pre-registered mock; creating MockProofNodeClient on the fly" ); - test_utils::register_mock_proof_engine(idx, 0) + test_utils::register_mock_proof_engine::(idx, 0) }); debug!(idx, "Instantiating mock proof engine from registry"); - Some(Arc::new(eip8025::HttpProofEngine::with_proof_node( - (*mock).clone(), - ))) + Some(Arc::new(eip8025::HttpProofEngine::with_proof_node(mock))) } else { debug!(endpoint = %proof_url, "Loaded proof engine endpoint"); Some(Arc::new(eip8025::HttpProofEngine::new(proof_url, None))) diff --git a/beacon_node/execution_layer/src/test_utils/mock_proof_node_client.rs b/beacon_node/execution_layer/src/test_utils/mock_proof_node_client.rs index 4b305e2b027..fe05e7c738f 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_proof_node_client.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_proof_node_client.rs @@ -10,25 +10,75 @@ use crate::eip8025::errors::ProofEngineError; use crate::eip8025::proof_node_client::ProofNodeClient; use crate::eip8025::types::{ProofComplete, ProofEvent}; -use crate::engine_api::NewPayloadRequestFulu; use bytes::Bytes; use futures::stream::Stream; use parking_lot::Mutex; -use ssz::{Encode, SszDecoderBuilder}; +use ssz::{Decode, Encode}; +use ssz_derive::{Decode as SszDecode, Encode as SszEncode}; use ssz_types::VariableList; use std::collections::HashMap; +use std::marker::PhantomData; use std::pin::Pin; use std::sync::{Arc, LazyLock}; use std::time::Duration; +use superstruct::superstruct; use tokio::sync::broadcast; use tokio_stream::StreamExt; use tokio_stream::wrappers::BroadcastStream; use tree_hash::TreeHash; +use tree_hash_derive::TreeHash as TreeHashDerive; use types::execution::eip8025::{ProofAttributes, ProofStatus}; use types::{ - EthSpec, ExecutionPayloadFulu, ExecutionRequests, Hash256, MainnetEthSpec, VersionedHash, + BeaconStateError, EthSpec, ExecutionPayloadBellatrix, ExecutionPayloadCapella, + ExecutionPayloadDeneb, ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionPayloadGloas, + ExecutionRequests, Hash256, MainnetEthSpec, VersionedHash, }; +/// Owned version of `NewPayloadRequest` used only for SSZ decoding inside the mock. +/// +/// The production `NewPayloadRequest<'block, E>` holds `&'block` references (zero-copy +/// during block processing), which prevents deriving `ssz::Decode`. This local owned +/// superstruct enum mirrors all fork variants with owned fields and is used exclusively +/// to decode the SSZ bytes sent to `request_proofs` and compute `tree_hash_root`. +#[superstruct( + variants(Bellatrix, Capella, Deneb, Electra, Fulu, Gloas), + variant_attributes(derive(SszEncode, SszDecode, TreeHashDerive)), + cast_error( + ty = "BeaconStateError", + expr = "BeaconStateError::IncorrectStateVariant" + ), + partial_getter_error( + ty = "BeaconStateError", + expr = "BeaconStateError::IncorrectStateVariant" + ) +)] +#[derive(SszEncode, SszDecode, TreeHashDerive)] +#[ssz(enum_behaviour = "transparent")] +#[tree_hash(enum_behaviour = "transparent")] +pub(crate) struct OwnedNewPayloadRequest { + #[superstruct( + only(Bellatrix), + partial_getter(rename = "execution_payload_bellatrix") + )] + pub execution_payload: ExecutionPayloadBellatrix, + #[superstruct(only(Capella), partial_getter(rename = "execution_payload_capella"))] + pub execution_payload: ExecutionPayloadCapella, + #[superstruct(only(Deneb), partial_getter(rename = "execution_payload_deneb"))] + pub execution_payload: ExecutionPayloadDeneb, + #[superstruct(only(Electra), partial_getter(rename = "execution_payload_electra"))] + pub execution_payload: ExecutionPayloadElectra, + #[superstruct(only(Fulu), partial_getter(rename = "execution_payload_fulu"))] + pub execution_payload: ExecutionPayloadFulu, + #[superstruct(only(Gloas), partial_getter(rename = "execution_payload_gloas"))] + pub execution_payload: ExecutionPayloadGloas, + #[superstruct(only(Deneb, Electra, Fulu, Gloas))] + pub versioned_hashes: VariableList, + #[superstruct(only(Deneb, Electra, Fulu, Gloas))] + pub parent_beacon_block_root: Hash256, + #[superstruct(only(Electra, Fulu, Gloas))] + pub execution_requests: ExecutionRequests, +} + /// Events emitted by [`MockProofNodeClient`] for each method invocation. /// /// Subscribe via [`MockProofNodeClient::subscribe_client_events`] to observe @@ -47,22 +97,50 @@ pub enum MockClientEvent { ProofFetched { root: Hash256, proof_type: u8 }, } -static MOCK_REGISTRY: LazyLock>>> = - LazyLock::new(|| parking_lot::Mutex::new(HashMap::new())); +/// The registry stores a concrete `MockProofNodeClient` as a +/// non-generic stand-in. All fields are Arc-wrapped, so `get_mock_proof_engine` +/// can construct a `MockProofNodeClient` for any `E` by sharing those Arcs. +static MOCK_REGISTRY: LazyLock< + parking_lot::Mutex>>>, +> = LazyLock::new(|| parking_lot::Mutex::new(HashMap::new())); /// Register a mock at `index`. Must be called before `ExecutionLayer::from_config`. -pub fn register_mock_proof_engine( +/// +/// Stores the mock as `MainnetEthSpec` internally and returns a `MockProofNodeClient` +/// that shares the same Arc-backed state but decodes SSZ using `E`. +pub fn register_mock_proof_engine( index: usize, callback_delay_ms: u64, -) -> Arc { - let client = Arc::new(MockProofNodeClient::new(callback_delay_ms)); - MOCK_REGISTRY.lock().insert(index, client.clone()); - client +) -> MockProofNodeClient { + let stored = Arc::new(MockProofNodeClient::::new( + callback_delay_ms, + )); + let typed = MockProofNodeClient:: { + requests: stored.requests.clone(), + event_tx: stored.event_tx.clone(), + call_tx: stored.call_tx.clone(), + callback_delay_ms: stored.callback_delay_ms, + _phantom: PhantomData, + }; + MOCK_REGISTRY.lock().insert(index, stored); + typed } -/// Fetch a registered mock by index (returns a clone sharing internal state). -pub fn get_mock_proof_engine(index: usize) -> Option> { - MOCK_REGISTRY.lock().get(&index).cloned() +/// Fetch a registered mock by index as a `MockProofNodeClient`. +/// +/// Constructs the typed client by sharing the Arc fields of the stored +/// `MockProofNodeClient`, so all state (requests, events) is shared. +pub fn get_mock_proof_engine(index: usize) -> Option> { + MOCK_REGISTRY + .lock() + .get(&index) + .map(|stored| MockProofNodeClient:: { + requests: stored.requests.clone(), + event_tx: stored.event_tx.clone(), + call_tx: stored.call_tx.clone(), + callback_delay_ms: stored.callback_delay_ms, + _phantom: PhantomData, + }) } /// URL encoding an index: `"http://mock/{n}/"`. @@ -82,61 +160,24 @@ pub fn parse_mock_index(url: &str) -> Option { }) } -/// Decode SSZ bytes as a `NewPayloadRequestFulu` and compute -/// the tree-hash root. -/// -/// Decodes each field individually via `SszDecoderBuilder`, constructs a -/// `NewPayloadRequestFulu` borrowing the owned fields, and returns the -/// tree-hash root of the real superstruct type. -fn decode_fulu_tree_hash_root(ssz_body: &[u8]) -> Result { - let mut builder = SszDecoderBuilder::new(ssz_body); - builder.register_type::>()?; - builder.register_type::::MaxBlobCommitmentsPerBlock>>()?; - builder.register_type::()?; - builder.register_type::>()?; - let mut decoder = builder.build()?; - - let execution_payload: ExecutionPayloadFulu = decoder.decode_next()?; - let versioned_hashes: VariableList< - VersionedHash, - ::MaxBlobCommitmentsPerBlock, - > = decoder.decode_next()?; - let parent_beacon_block_root: Hash256 = decoder.decode_next()?; - let execution_requests: ExecutionRequests = decoder.decode_next()?; - - let request = NewPayloadRequestFulu { - execution_payload: &execution_payload, - versioned_hashes, - parent_beacon_block_root, - execution_requests: &execution_requests, - }; - Ok(request.tree_hash_root()) -} - -/// Build a test SSZ body encoding a `NewPayloadRequestFulu` with the given +/// Build a test SSZ body encoding a `NewPayloadRequestFulu` with the given /// parent beacon block root. Returns `(ssz_bytes, expected_tree_hash_root)`. -pub fn make_test_fulu_ssz(parent_root: Hash256) -> (Vec, Hash256) { - let execution_payload = ExecutionPayloadFulu::::default(); - let versioned_hashes = VariableList::< - VersionedHash, - ::MaxBlobCommitmentsPerBlock, - >::default(); - let execution_requests = ExecutionRequests::::default(); - let request = NewPayloadRequestFulu { - execution_payload: &execution_payload, - versioned_hashes, +pub fn make_test_fulu_ssz(parent_root: Hash256) -> (Vec, Hash256) { + let request = OwnedNewPayloadRequestFulu:: { + execution_payload: ExecutionPayloadFulu::default(), + versioned_hashes: VariableList::default(), parent_beacon_block_root: parent_root, - execution_requests: &execution_requests, + execution_requests: ExecutionRequests::default(), }; + let request = OwnedNewPayloadRequest::Fulu(request); (request.as_ssz_bytes(), request.tree_hash_root()) } -/// In-memory proof node client for testing. +/// In-memory proof node client for testing, generic over [`EthSpec`]. /// -/// Each call to [`request_proofs`] decodes the SSZ body as a Fulu -/// `NewPayloadRequest`, computes the tree-hash root, records the raw SSZ body, -/// and schedules a [`ProofEvent::ProofComplete`] event for each requested -/// proof type after `callback_delay_ms` milliseconds. +/// Each call to [`request_proofs`] decodes the SSZ body using `E`, records the +/// raw SSZ body, and schedules a [`ProofEvent::ProofComplete`] event for each +/// requested proof type after `callback_delay_ms` milliseconds. /// /// Call [`subscribe_client_events`] to receive a [`MockClientEvent`] stream /// that fires once per method invocation — useful for asserting that the proof @@ -144,8 +185,7 @@ pub fn make_test_fulu_ssz(parent_root: Hash256) -> (Vec, Hash256) { /// /// [`request_proofs`]: MockProofNodeClient::request_proofs /// [`subscribe_client_events`]: MockProofNodeClient::subscribe_client_events -#[derive(Clone)] -pub struct MockProofNodeClient { +pub struct MockProofNodeClient { /// Received SSZ request bodies in order of arrival. requests: Arc>>>, /// Broadcast channel for in-memory SSE events. @@ -154,10 +194,23 @@ pub struct MockProofNodeClient { call_tx: broadcast::Sender, /// Delay in milliseconds before broadcasting proof complete events. callback_delay_ms: u64, + _phantom: PhantomData, +} + +impl Clone for MockProofNodeClient { + fn clone(&self) -> Self { + Self { + requests: self.requests.clone(), + event_tx: self.event_tx.clone(), + call_tx: self.call_tx.clone(), + callback_delay_ms: self.callback_delay_ms, + _phantom: PhantomData, + } + } } -impl MockProofNodeClient { - /// Create a new mock client. +impl MockProofNodeClient { + /// Create a new unregistered mock client. /// /// `callback_delay_ms` controls how long after `request_proofs` the /// proof complete events are broadcast. @@ -169,6 +222,7 @@ impl MockProofNodeClient { event_tx, call_tx, callback_delay_ms, + _phantom: PhantomData, } } @@ -193,14 +247,15 @@ impl MockProofNodeClient { } #[async_trait::async_trait] -impl ProofNodeClient for MockProofNodeClient { +impl ProofNodeClient for MockProofNodeClient { async fn request_proofs( &self, ssz_body: Vec, proof_attributes: ProofAttributes, ) -> Result { - let root = decode_fulu_tree_hash_root(&ssz_body) - .map_err(|e| ProofEngineError::InvalidPayload(format!("SSZ decode failed: {e:?}")))?; + let root = OwnedNewPayloadRequest::::from_ssz_bytes(&ssz_body) + .map_err(|e| ProofEngineError::InvalidPayload(format!("SSZ decode failed: {e:?}")))? + .tree_hash_root(); self.requests.lock().push(ssz_body.clone()); diff --git a/beacon_node/execution_layer/src/test_utils/mod.rs b/beacon_node/execution_layer/src/test_utils/mod.rs index fd357737ce1..2f492658515 100644 --- a/beacon_node/execution_layer/src/test_utils/mod.rs +++ b/beacon_node/execution_layer/src/test_utils/mod.rs @@ -77,7 +77,7 @@ mod handle_rpc; mod hook; mod mock_builder; mod mock_execution_layer; -mod mock_proof_node_client; +pub(crate) mod mock_proof_node_client; /// Configuration for the MockExecutionLayer. #[derive(Clone)] diff --git a/beacon_node/lighthouse_network/src/discovery/enr.rs b/beacon_node/lighthouse_network/src/discovery/enr.rs index ce4be57f6d0..1f43f66642e 100644 --- a/beacon_node/lighthouse_network/src/discovery/enr.rs +++ b/beacon_node/lighthouse_network/src/discovery/enr.rs @@ -30,7 +30,7 @@ pub const SYNC_COMMITTEE_BITFIELD_ENR_KEY: &str = "syncnets"; /// The ENR field specifying the peerdas custody group count. pub const PEERDAS_CUSTODY_GROUP_COUNT_ENR_KEY: &str = "cgc"; /// The ENR field indicating execution proof engine support. -pub const EXECUTION_PROOF_ENR_KEY: &str = "ep"; +pub const EXECUTION_PROOF_ENR_KEY: &str = "eproof"; /// Extension trait for ENR's within Eth2. pub trait Eth2Enr { diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 165e2e5bc32..8ea5f1e12f8 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -21,6 +21,7 @@ use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::{BeaconChain, BeaconChainTypes, BlockProcessStatus, EngineState}; use custody::CustodyRequestResult; use fnv::FnvHashMap; +use lighthouse_network::Eth2Enr; use lighthouse_network::rpc::methods::{ BlobsByRangeRequest, DataColumnsByRangeRequest, ExecutionProofStatus, ExecutionProofsByRangeRequest, ExecutionProofsByRootRequest, @@ -34,7 +35,6 @@ use lighthouse_network::service::api_types::{ DataColumnsByRootRequester, ExecutionProofStatusRequestId, ExecutionProofsByRangeRequestId, ExecutionProofsByRootRequestId, Id, SingleLookupReqId, SyncRequestId, }; -use lighthouse_network::types::Subnet; use lighthouse_network::{Client, NetworkGlobals, PeerAction, PeerId, ReportSource}; use lighthouse_tracing::{SPAN_OUTGOING_BLOCK_BY_ROOT_REQUEST, SPAN_OUTGOING_RANGE_REQUEST}; use parking_lot::RwLock; @@ -508,7 +508,11 @@ impl SyncNetworkContext { .peers .read() .peer_info(peer_id) - .is_some_and(|info| info.on_subnet_metadata(&Subnet::ExecutionProof)) + .is_some_and(|info| { + info.enr() + .map(|enr| enr.execution_proof_enabled()) + .unwrap_or(false) + }) } /// Returns the Client type of the peer if known diff --git a/testing/proof_engine/src/lib.rs b/testing/proof_engine/src/lib.rs index 083c577f890..19ac481579d 100644 --- a/testing/proof_engine/src/lib.rs +++ b/testing/proof_engine/src/lib.rs @@ -36,6 +36,7 @@ mod test { extra_nodes: 0, proof_generator_nodes: 1, proof_verifier_nodes: 1, + delayed_nodes: 0, genesis_delay: 120, }) } @@ -57,15 +58,18 @@ mod test { .proof_generator_subscribe_client_events() .expect("proof generator node should expose a mock client event stream"); - tokio::time::sleep(Duration::from_secs(60)).await; - - // Drain and count ProofRequested events. - let mut proof_requests = 0usize; - while let Ok(event) = event_rx.try_recv() { - if matches!(event, MockClientEvent::ProofRequested { .. }) { - proof_requests += 1; + let proof_requests = tokio::time::timeout(Duration::from_secs(30), async { + let mut proof_request_count: u64 = 0; + loop { + if let Ok(MockClientEvent::ProofRequested { .. }) = event_rx.recv().await { proof_request_count += 1 } + if proof_request_count > 0 { + break; + } } - } + proof_request_count + }) + .await?; + assert!( proof_requests > 0, "expected at least one proof request after 60s" @@ -97,6 +101,7 @@ mod test { }) .map_network_params(|params| { params.proof_verifier_nodes = 0; + params.delayed_nodes = 1; }) .with_log_level(LevelFilter::DEBUG) .with_log_dir("proof-engine-sync".into()) @@ -105,7 +110,7 @@ mod test { fixture.payloads_valid(); fixture.wait_for_genesis().await?; - tokio::time::sleep(Duration::from_secs(60)).await; + tokio::time::sleep(Duration::from_secs(30)).await; // Now lets add a new proof verifier node and observe the sync behaviour. let net = fixture.network.clone(); diff --git a/testing/simulator/Cargo.toml b/testing/simulator/Cargo.toml index 930025ea434..ae1b484a45f 100644 --- a/testing/simulator/Cargo.toml +++ b/testing/simulator/Cargo.toml @@ -19,6 +19,7 @@ futures = { workspace = true } kzg = { workspace = true } lighthouse_network = { workspace = true } logging = { workspace = true } +network_utils = { workspace = true } node_test_rig = { path = "../node_test_rig" } parking_lot = { workspace = true } rayon = { workspace = true } diff --git a/testing/simulator/src/basic_sim.rs b/testing/simulator/src/basic_sim.rs index 7666c5e6e99..cc1c3c32a01 100644 --- a/testing/simulator/src/basic_sim.rs +++ b/testing/simulator/src/basic_sim.rs @@ -212,6 +212,7 @@ pub fn run_basic_sim(matches: &ArgMatches) -> Result<(), String> { genesis_delay, proof_generator_nodes: 0, proof_verifier_nodes: 0, + delayed_nodes: 0, }, context.clone(), )) diff --git a/testing/simulator/src/fallback_sim.rs b/testing/simulator/src/fallback_sim.rs index d80d344601a..372290a3524 100644 --- a/testing/simulator/src/fallback_sim.rs +++ b/testing/simulator/src/fallback_sim.rs @@ -217,6 +217,7 @@ pub fn run_fallback_sim(matches: &ArgMatches) -> Result<(), String> { proposer_nodes: 0, proof_generator_nodes: 0, proof_verifier_nodes: 0, + delayed_nodes: 0, genesis_delay, }, context.clone(), diff --git a/testing/simulator/src/local_network.rs b/testing/simulator/src/local_network.rs index c7d027d5ae2..de16a65a4d6 100644 --- a/testing/simulator/src/local_network.rs +++ b/testing/simulator/src/local_network.rs @@ -2,6 +2,7 @@ use crate::checks::epoch_delay; use beacon_chain::custody_context::NodeCustodyType; use kzg::trusted_setup::get_trusted_setup; use lighthouse_network::types::Enr; +use network_utils::listen_addr::ListenAddress; use node_test_rig::{ ClientConfig, ClientGenesis, LocalBeaconNode, LocalExecutionNode, LocalValidatorClient, MockExecutionConfig, ValidatorConfig, ValidatorFiles, @@ -70,6 +71,7 @@ pub struct LocalNetworkParams { pub proof_generator_nodes: usize, pub proof_verifier_nodes: usize, pub extra_nodes: usize, + pub delayed_nodes: usize, pub genesis_delay: u64, } @@ -106,6 +108,7 @@ fn default_client_config(network_params: LocalNetworkParams, genesis_time: u64) + network_params.proof_generator_nodes + network_params.proof_verifier_nodes + network_params.extra_nodes + + network_params.delayed_nodes - 1; beacon_config.network.enr_address = (Some(Ipv4Addr::LOCALHOST), None); beacon_config.network.enable_light_client_server = true; @@ -253,15 +256,21 @@ impl LocalNetwork { mut beacon_config: ClientConfig, mock_execution_config: MockExecutionConfig, ) -> Result<(LocalBeaconNode, LocalExecutionNode), String> { + let listen = ListenAddress::unused_v4_ports(); + let v4 = listen.v4().expect("unused_v4_ports always returns V4"); + beacon_config.network.set_ipv4_listening_address( + Ipv4Addr::UNSPECIFIED, + v4.tcp_port, + v4.disc_port, + v4.quic_port, + ); beacon_config.network.discv5_config.table_filter = |_| true; + beacon_config.network.enr_udp4_port = std::num::NonZeroU16::new(v4.disc_port); + beacon_config.network.enr_tcp4_port = std::num::NonZeroU16::new(v4.tcp_port); + beacon_config.network.enr_quic4_port = std::num::NonZeroU16::new(v4.quic_port); // The boot node is a full data-availability node and should custody all columns from - // genesis. Setting Supernode ensures cgc = number_of_custody_groups from startup so - // no validator-registration-triggered cgc jump occurs. Without this, the first proposer - // preparation call from the validator client causes cgc to increase from - // spec.custody_requirement → number_of_custody_groups, which stamps - // earliest_available_slot = current_slot and prevents late-joining nodes from syncing - // from epoch 0. + // genesis. This ensures we have sufficient peers on each custody group. beacon_config.chain.node_custody_type = NodeCustodyType::Supernode; let execution_node = LocalExecutionNode::new(self.context.clone(), mock_execution_config); @@ -284,7 +293,18 @@ impl LocalNetwork { mock_execution_config: MockExecutionConfig, node_type: NodeType, ) -> Result<(LocalBeaconNode, Option>), String> { + let listen = ListenAddress::unused_v4_ports(); + let v4 = listen.v4().expect("unused_v4_ports always returns V4"); + beacon_config.network.set_ipv4_listening_address( + Ipv4Addr::UNSPECIFIED, + v4.tcp_port, + v4.disc_port, + v4.quic_port, + ); beacon_config.network.discv5_config.table_filter = |_| true; + beacon_config.network.enr_udp4_port = std::num::NonZeroU16::new(v4.disc_port); + beacon_config.network.enr_tcp4_port = std::num::NonZeroU16::new(v4.tcp_port); + beacon_config.network.enr_quic4_port = std::num::NonZeroU16::new(v4.quic_port); beacon_config.network.proposer_only = node_type.is_proposer(); let execution_node = if node_type.requires_execution_node() { @@ -307,11 +327,10 @@ impl LocalNetwork { }; if node_type.requires_proof_node() { - // Subscribe to the execution_proof gossip topic and wire up the mock proof engine. beacon_config.network.enable_execution_proof = true; - // Index = current length of beacon_nodes (this node's future position in the list). let bn_idx = self.beacon_nodes.read().len(); - execution_layer::test_utils::register_mock_proof_engine(bn_idx, 0); + let _: execution_layer::test_utils::MockProofNodeClient = + execution_layer::test_utils::register_mock_proof_engine(bn_idx, 0); let mock_url = SensitiveUrl::parse(&execution_layer::test_utils::mock_proof_engine_url(bn_idx)) .expect("mock URL is valid"); @@ -327,9 +346,6 @@ impl LocalNetwork { if node_type.is_proof_verifier() { beacon_config.chain.optimistic_finalized_sync = true; - beacon_config.network.boot_nodes_enr.push(self.proof_generator_enr().ok_or_else(|| { - "Proof verifier node requires a proof generator node to connect to, but no proof generator node found in the network".to_string() - })?); } // Construct beacon node using the config, @@ -338,13 +354,6 @@ impl LocalNetwork { Ok((beacon_node, execution_node)) } - pub fn proof_generator_enr(&self) -> Option { - self.beacon_nodes - .read() - .last() - .and_then(|bn| bn.client.enr()) - } - /// Returns the boot node's ENR once it has a valid (non-zero) TCP port, or an error if /// the port isn't populated within 10 seconds. async fn boot_node_enr(&self) -> Result, String> { @@ -359,13 +368,13 @@ impl LocalNetwork { .read() .first() .and_then(|bn| bn.client.enr()) - .filter(|e| e.tcp4().is_some_and(|p| p != 0)) + .filter(|e| e.tcp4().is_some_and(|p| p != 0) && e.udp4().is_some_and(|p| p != 0)) { return Ok(Some(enr)); } tokio::time::sleep(Duration::from_millis(100)).await; } - Err("Boot node ENR did not get a valid TCP port within 10 seconds".to_string()) + Err("Boot node ENR did not get valid TCP and UDP ports within 10 seconds".to_string()) } /// Adds a beacon node to the network, connecting to the 0'th beacon node via ENR. diff --git a/testing/simulator/src/test_utils/builder.rs b/testing/simulator/src/test_utils/builder.rs index ffa2f27ef63..6c68bfa4f57 100644 --- a/testing/simulator/src/test_utils/builder.rs +++ b/testing/simulator/src/test_utils/builder.rs @@ -21,6 +21,7 @@ impl Default for TestNetworkFixtureBuilder { proof_generator_nodes: 0, proof_verifier_nodes: 0, extra_nodes: 0, + delayed_nodes: 0, genesis_delay: 38, }, logger_config: LoggerConfig::default(), diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index e3f80391665..e62254cad2a 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -540,15 +540,15 @@ impl ProductionValidatorClient { let url_str = endpoint.expose_full(); let proof_engine_client = Arc::new( if let Some(idx) = execution_layer::test_utils::parse_mock_index(url_str.as_str()) { - let mock = execution_layer::test_utils::get_mock_proof_engine(idx) + let mock = execution_layer::test_utils::get_mock_proof_engine::(idx) .unwrap_or_else(|| { debug!( idx, "No pre-registered mock; creating MockProofNodeClient on the fly" ); - execution_layer::test_utils::register_mock_proof_engine(idx, 0) + execution_layer::test_utils::register_mock_proof_engine::(idx, 0) }); - execution_layer::eip8025::HttpProofEngine::with_proof_node((*mock).clone()) + execution_layer::eip8025::HttpProofEngine::with_proof_node(mock) } else { execution_layer::eip8025::HttpProofEngine::new(endpoint.clone(), None) }, From bae363d72871d441bdad99ed56d68bdaab428f44 Mon Sep 17 00:00:00 2001 From: frisitano Date: Tue, 24 Mar 2026 18:00:48 +0100 Subject: [PATCH 2/2] lint --- testing/proof_engine/src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/testing/proof_engine/src/lib.rs b/testing/proof_engine/src/lib.rs index 19ac481579d..8edcfe6b360 100644 --- a/testing/proof_engine/src/lib.rs +++ b/testing/proof_engine/src/lib.rs @@ -61,7 +61,9 @@ mod test { let proof_requests = tokio::time::timeout(Duration::from_secs(30), async { let mut proof_request_count: u64 = 0; loop { - if let Ok(MockClientEvent::ProofRequested { .. }) = event_rx.recv().await { proof_request_count += 1 } + if let Ok(MockClientEvent::ProofRequested { .. }) = event_rx.recv().await { + proof_request_count += 1 + } if proof_request_count > 0 { break; }