From 0ad5bf63d7dffa303d80cb95142a2989c321a7e3 Mon Sep 17 00:00:00 2001 From: SamAg19 Date: Sun, 6 Sep 2026 03:05:41 +0530 Subject: [PATCH 1/9] Add engine_getPayloadBodiesByHashV2 engine API support --- beacon_node/execution_layer/src/engine_api.rs | 19 ++++--- .../execution_layer/src/engine_api/http.rs | 28 ++++++++++ .../src/engine_api/json_structures.rs | 52 ++++++++++++++++++- beacon_node/execution_layer/src/lib.rs | 9 +++- .../src/test_utils/handle_rpc.rs | 4 +- .../execution_layer/src/test_utils/mod.rs | 1 + 6 files changed, 102 insertions(+), 11 deletions(-) diff --git a/beacon_node/execution_layer/src/engine_api.rs b/beacon_node/execution_layer/src/engine_api.rs index 52a58c88fec..2328c73a7af 100644 --- a/beacon_node/execution_layer/src/engine_api.rs +++ b/beacon_node/execution_layer/src/engine_api.rs @@ -3,10 +3,10 @@ use crate::http::{ ENGINE_FORKCHOICE_UPDATED_V1, ENGINE_FORKCHOICE_UPDATED_V2, ENGINE_FORKCHOICE_UPDATED_V3, ENGINE_FORKCHOICE_UPDATED_V4, ENGINE_GET_BLOBS_V2, ENGINE_GET_CLIENT_VERSION_V1, ENGINE_GET_INCLUSION_LIST_V1, ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, - ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, ENGINE_GET_PAYLOAD_V1, ENGINE_GET_PAYLOAD_V2, - ENGINE_GET_PAYLOAD_V3, ENGINE_GET_PAYLOAD_V4, ENGINE_GET_PAYLOAD_V5, ENGINE_GET_PAYLOAD_V6, - ENGINE_NEW_PAYLOAD_V1, ENGINE_NEW_PAYLOAD_V2, ENGINE_NEW_PAYLOAD_V3, ENGINE_NEW_PAYLOAD_V4, - ENGINE_NEW_PAYLOAD_V5, + ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V2, ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, + ENGINE_GET_PAYLOAD_V1, ENGINE_GET_PAYLOAD_V2, ENGINE_GET_PAYLOAD_V3, ENGINE_GET_PAYLOAD_V4, + ENGINE_GET_PAYLOAD_V5, ENGINE_GET_PAYLOAD_V6, ENGINE_NEW_PAYLOAD_V1, ENGINE_NEW_PAYLOAD_V2, + ENGINE_NEW_PAYLOAD_V3, ENGINE_NEW_PAYLOAD_V4, ENGINE_NEW_PAYLOAD_V5, }; use eth2::types::{ BlobsBundle, SsePayloadAttributes, SsePayloadAttributesV1, SsePayloadAttributesV2, @@ -20,9 +20,9 @@ use serde::{Deserialize, Serialize}; use strum::IntoStaticStr; use superstruct::superstruct; pub use types::{ - Address, BeaconBlockRef, ConsolidationRequest, EthSpec, ExecutionBlockHash, ExecutionPayload, - ExecutionPayloadHeader, ExecutionPayloadRef, ForkName, Hash256, Transactions, Uint256, - Withdrawal, Withdrawals, + Address, BeaconBlockRef, BlockAccessList, ConsolidationRequest, EthSpec, ExecutionBlockHash, + ExecutionPayload, ExecutionPayloadHeader, ExecutionPayloadRef, ForkName, Hash256, Transactions, + Uint256, Withdrawal, Withdrawals, }; use types::{ ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadDeneb, @@ -447,6 +447,7 @@ impl GetPayloadResponse { pub struct ExecutionPayloadBodyV1 { pub transactions: Transactions, pub withdrawals: Option>, + pub block_access_list: Option, } impl ExecutionPayloadBodyV1 { @@ -605,6 +606,7 @@ pub struct EngineCapabilities { pub forkchoice_updated_v3: bool, pub forkchoice_updated_v4: bool, pub get_payload_bodies_by_hash_v1: bool, + pub get_payload_bodies_by_hash_v2: bool, pub get_payload_bodies_by_range_v1: bool, pub get_payload_v1: bool, pub get_payload_v2: bool, @@ -651,6 +653,9 @@ impl EngineCapabilities { if self.get_payload_bodies_by_hash_v1 { response.push(ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1); } + if self.get_payload_bodies_by_hash_v2 { + response.push(ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V2); + } if self.get_payload_bodies_by_range_v1 { response.push(ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1); } diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index b6e755471e0..d1259d1a663 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -55,6 +55,7 @@ pub const ENGINE_FORKCHOICE_UPDATED_V4: &str = "engine_forkchoiceUpdatedV4"; pub const ENGINE_FORKCHOICE_UPDATED_TIMEOUT: Duration = Duration::from_secs(8); pub const ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1: &str = "engine_getPayloadBodiesByHashV1"; +pub const ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V2: &str = "engine_getPayloadBodiesByHashV2"; pub const ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1: &str = "engine_getPayloadBodiesByRangeV1"; pub const ENGINE_GET_PAYLOAD_BODIES_TIMEOUT: Duration = Duration::from_secs(10); @@ -94,6 +95,7 @@ pub static LIGHTHOUSE_CAPABILITIES: &[&str] = &[ ENGINE_FORKCHOICE_UPDATED_V3, ENGINE_FORKCHOICE_UPDATED_V4, ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, + ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V2, ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, ENGINE_GET_CLIENT_VERSION_V1, ENGINE_GET_BLOBS_V2, @@ -1204,6 +1206,30 @@ impl HttpJsonRpc { .collect::, _>>() } + pub async fn get_payload_bodies_by_hash_v2( + &self, + block_hashes: Vec, + ) -> Result>>, Error> { + let params = json!([block_hashes]); + + let response: Vec>> = self + .rpc_request( + ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V2, + params, + ENGINE_GET_PAYLOAD_BODIES_TIMEOUT * self.execution_timeout_multiplier, + ) + .await?; + + response + .into_iter() + .map(|opt_json| { + opt_json + .map(|json| json.try_into().map_err(Error::from)) + .transpose() + }) + .collect::, _>>() + } + pub async fn get_payload_bodies_by_range_v1( &self, start: u64, @@ -1255,6 +1281,8 @@ impl HttpJsonRpc { forkchoice_updated_v4: capabilities.contains(ENGINE_FORKCHOICE_UPDATED_V4), get_payload_bodies_by_hash_v1: capabilities .contains(ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1), + get_payload_bodies_by_hash_v2: capabilities + .contains(ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V2), get_payload_bodies_by_range_v1: capabilities .contains(ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1), get_payload_v1: capabilities.contains(ENGINE_GET_PAYLOAD_V1), diff --git a/beacon_node/execution_layer/src/engine_api/json_structures.rs b/beacon_node/execution_layer/src/engine_api/json_structures.rs index b6f5f820034..3a1ad2a77fe 100644 --- a/beacon_node/execution_layer/src/engine_api/json_structures.rs +++ b/beacon_node/execution_layer/src/engine_api/json_structures.rs @@ -1208,11 +1208,19 @@ impl From for JsonForkchoiceUpdatedV1Response { } #[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(bound = "E: EthSpec")] +#[serde(transparent)] +pub struct JsonBlockAccessList( + #[serde(with = "ssz_types::serde_utils::hex_prog_var_list")] pub BlockAccessList, +); + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(bound = "E: EthSpec", rename_all = "camelCase")] pub struct JsonExecutionPayloadBodyV1 { #[serde(with = "ssz_types::serde_utils::list_of_hex_var_list")] pub transactions: Transactions, pub withdrawals: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub block_access_list: Option, } impl TryFrom> for ExecutionPayloadBodyV1 { @@ -1222,6 +1230,7 @@ impl TryFrom> for ExecutionPayloadBody Ok(Self { transactions: value.transactions, withdrawals: value.withdrawals.map(withdrawals_from_json).transpose()?, + block_access_list: value.block_access_list.map(|list| list.0), }) } } @@ -1233,6 +1242,7 @@ impl TryFrom> for JsonExecutionPayloadBody Ok(Self { transactions: value.transactions, withdrawals: value.withdrawals.map(withdrawals_to_json).transpose()?, + block_access_list: value.block_access_list.map(JsonBlockAccessList), }) } } @@ -1651,4 +1661,44 @@ mod tests { RequestsError::EmptyRequest(0) )); } + + #[test] + fn payload_body_block_access_list_round_trip() { + use serde_json::json; + + // Present `blockAccessList` -> `Some`. + let with_bal = json!({ + "transactions": [], + "withdrawals": null, + "blockAccessList": "0x010203", + }); + let body: JsonExecutionPayloadBodyV1 = + serde_json::from_value(with_bal.clone()).unwrap(); + let internal: ExecutionPayloadBodyV1 = body.clone().try_into().unwrap(); + assert_eq!( + internal.block_access_list, + Some(ProgressiveVariableList::new(vec![1, 2, 3])) + ); + assert_eq!(serde_json::to_value(&body).unwrap(), with_bal); + + // Explicit `null` -> `None`, omitted on re-serialize. + let null_bal = json!({ + "transactions": [], + "withdrawals": null, + "blockAccessList": null, + }); + let body: JsonExecutionPayloadBodyV1 = + serde_json::from_value(null_bal).unwrap(); + let internal: ExecutionPayloadBodyV1 = body.clone().try_into().unwrap(); + assert_eq!(internal.block_access_list, None); + assert_eq!( + serde_json::to_value(&body).unwrap(), + json!({ "transactions": [], "withdrawals": null }) + ); + + // Omitted -> `None`. + let body: JsonExecutionPayloadBodyV1 = + serde_json::from_value(json!({ "transactions": [], "withdrawals": null })).unwrap(); + assert!(body.block_access_list.is_none()); + } } diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index 29236ca7687..ba96cd0b12d 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -1665,9 +1665,14 @@ impl ExecutionLayer { &self, hashes: Vec, ) -> Result>>, Error> { + let capabilities = self.get_engine_capabilities(None).await?; self.engine() - .request(|engine: &Engine| async move { - engine.api.get_payload_bodies_by_hash_v1(hashes).await + .request(|engine| async move { + if capabilities.get_payload_bodies_by_hash_v2 { + engine.api.get_payload_bodies_by_hash_v2(hashes).await + } else { + engine.api.get_payload_bodies_by_hash_v1(hashes).await + } }) .await .map_err(Box::new) diff --git a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs index c1e72a6f3ed..ecc01214c42 100644 --- a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs +++ b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs @@ -724,7 +724,7 @@ pub async fn handle_rpc( ENGINE_GET_CLIENT_VERSION_V1 => { Ok(serde_json::to_value([DEFAULT_CLIENT_VERSION.clone()]).unwrap()) } - ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1 => { + ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1 | ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V2 => { let block_hashes = get_param::>(params, 0) .map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?; @@ -757,6 +757,7 @@ pub async fn handle_rpc( }) .transpose() .unwrap(), + block_access_list: payload.block_access_list().ok().cloned(), }; let json_payload_body: JsonExecutionPayloadBodyV1 = payload_body.try_into().unwrap(); @@ -809,6 +810,7 @@ pub async fn handle_rpc( }) .transpose() .unwrap(), + block_access_list: None, }; let json_payload_body: JsonExecutionPayloadBodyV1 = payload_body.try_into().unwrap(); diff --git a/beacon_node/execution_layer/src/test_utils/mod.rs b/beacon_node/execution_layer/src/test_utils/mod.rs index 82b55551367..2bdc0d11c07 100644 --- a/beacon_node/execution_layer/src/test_utils/mod.rs +++ b/beacon_node/execution_layer/src/test_utils/mod.rs @@ -49,6 +49,7 @@ pub const DEFAULT_ENGINE_CAPABILITIES: EngineCapabilities = EngineCapabilities { forkchoice_updated_v3: true, forkchoice_updated_v4: true, get_payload_bodies_by_hash_v1: true, + get_payload_bodies_by_hash_v2: true, get_payload_bodies_by_range_v1: true, get_payload_v1: true, get_payload_v2: true, From 726f9640117baffff017ca72dc22224ec7f6004c Mon Sep 17 00:00:00 2001 From: SamAg19 Date: Sun, 6 Sep 2026 03:16:13 +0530 Subject: [PATCH 2/9] Add engine_getPayloadBodiesByRangeV2 engine API support --- beacon_node/execution_layer/src/engine_api.rs | 11 +++++-- .../execution_layer/src/engine_api/http.rs | 32 +++++++++++++++++++ beacon_node/execution_layer/src/lib.rs | 18 ++++++++--- .../src/test_utils/handle_rpc.rs | 4 +-- .../execution_layer/src/test_utils/mod.rs | 1 + 5 files changed, 56 insertions(+), 10 deletions(-) diff --git a/beacon_node/execution_layer/src/engine_api.rs b/beacon_node/execution_layer/src/engine_api.rs index 2328c73a7af..3aefc3ff673 100644 --- a/beacon_node/execution_layer/src/engine_api.rs +++ b/beacon_node/execution_layer/src/engine_api.rs @@ -4,9 +4,10 @@ use crate::http::{ ENGINE_FORKCHOICE_UPDATED_V4, ENGINE_GET_BLOBS_V2, ENGINE_GET_CLIENT_VERSION_V1, ENGINE_GET_INCLUSION_LIST_V1, ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V2, ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, - ENGINE_GET_PAYLOAD_V1, ENGINE_GET_PAYLOAD_V2, ENGINE_GET_PAYLOAD_V3, ENGINE_GET_PAYLOAD_V4, - ENGINE_GET_PAYLOAD_V5, ENGINE_GET_PAYLOAD_V6, ENGINE_NEW_PAYLOAD_V1, ENGINE_NEW_PAYLOAD_V2, - ENGINE_NEW_PAYLOAD_V3, ENGINE_NEW_PAYLOAD_V4, ENGINE_NEW_PAYLOAD_V5, + ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V2, ENGINE_GET_PAYLOAD_V1, ENGINE_GET_PAYLOAD_V2, + ENGINE_GET_PAYLOAD_V3, ENGINE_GET_PAYLOAD_V4, ENGINE_GET_PAYLOAD_V5, ENGINE_GET_PAYLOAD_V6, + ENGINE_NEW_PAYLOAD_V1, ENGINE_NEW_PAYLOAD_V2, ENGINE_NEW_PAYLOAD_V3, ENGINE_NEW_PAYLOAD_V4, + ENGINE_NEW_PAYLOAD_V5, }; use eth2::types::{ BlobsBundle, SsePayloadAttributes, SsePayloadAttributesV1, SsePayloadAttributesV2, @@ -608,6 +609,7 @@ pub struct EngineCapabilities { pub get_payload_bodies_by_hash_v1: bool, pub get_payload_bodies_by_hash_v2: bool, pub get_payload_bodies_by_range_v1: bool, + pub get_payload_bodies_by_range_v2: bool, pub get_payload_v1: bool, pub get_payload_v2: bool, pub get_payload_v3: bool, @@ -659,6 +661,9 @@ impl EngineCapabilities { if self.get_payload_bodies_by_range_v1 { response.push(ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1); } + if self.get_payload_bodies_by_range_v2 { + response.push(ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V2); + } if self.get_payload_v1 { response.push(ENGINE_GET_PAYLOAD_V1); } diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index d1259d1a663..3dfbdede8b9 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -57,6 +57,7 @@ pub const ENGINE_FORKCHOICE_UPDATED_TIMEOUT: Duration = Duration::from_secs(8); pub const ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1: &str = "engine_getPayloadBodiesByHashV1"; pub const ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V2: &str = "engine_getPayloadBodiesByHashV2"; pub const ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1: &str = "engine_getPayloadBodiesByRangeV1"; +pub const ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V2: &str = "engine_getPayloadBodiesByRangeV2"; pub const ENGINE_GET_PAYLOAD_BODIES_TIMEOUT: Duration = Duration::from_secs(10); pub const ENGINE_EXCHANGE_CAPABILITIES: &str = "engine_exchangeCapabilities"; @@ -97,6 +98,7 @@ pub static LIGHTHOUSE_CAPABILITIES: &[&str] = &[ ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V2, ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, + ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V2, ENGINE_GET_CLIENT_VERSION_V1, ENGINE_GET_BLOBS_V2, ENGINE_GET_BLOBS_V3, @@ -1258,6 +1260,34 @@ impl HttpJsonRpc { .collect::, _>>() } + pub async fn get_payload_bodies_by_range_v2( + &self, + start: u64, + count: u64, + ) -> Result>>, Error> { + #[derive(Serialize)] + #[serde(transparent)] + struct Quantity(#[serde(with = "serde_utils::u64_hex_be")] u64); + + let params = json!([Quantity(start), Quantity(count)]); + let response: Vec>> = self + .rpc_request( + ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V2, + params, + ENGINE_GET_PAYLOAD_BODIES_TIMEOUT * self.execution_timeout_multiplier, + ) + .await?; + + response + .into_iter() + .map(|opt_json| { + opt_json + .map(|json| json.try_into().map_err(Error::from)) + .transpose() + }) + .collect::, _>>() + } + pub async fn exchange_capabilities(&self) -> Result { let params = json!([LIGHTHOUSE_CAPABILITIES]); @@ -1285,6 +1315,8 @@ impl HttpJsonRpc { .contains(ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V2), get_payload_bodies_by_range_v1: capabilities .contains(ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1), + get_payload_bodies_by_range_v2: capabilities + .contains(ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V2), get_payload_v1: capabilities.contains(ENGINE_GET_PAYLOAD_V1), get_payload_v2: capabilities.contains(ENGINE_GET_PAYLOAD_V2), get_payload_v3: capabilities.contains(ENGINE_GET_PAYLOAD_V3), diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index ba96cd0b12d..e3adfd1542a 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -1684,13 +1684,21 @@ impl ExecutionLayer { start: u64, count: u64, ) -> Result>>, Error> { + let capabilities = self.get_engine_capabilities(None).await?; let _timer = metrics::start_timer(&metrics::EXECUTION_LAYER_GET_PAYLOAD_BODIES_BY_RANGE); self.engine() - .request(|engine: &Engine| async move { - engine - .api - .get_payload_bodies_by_range_v1(start, count) - .await + .request(|engine| async move { + if capabilities.get_payload_bodies_by_range_v2 { + engine + .api + .get_payload_bodies_by_range_v2(start, count) + .await + } else { + engine + .api + .get_payload_bodies_by_range_v1(start, count) + .await + } }) .await .map_err(Box::new) diff --git a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs index ecc01214c42..16ad14175c1 100644 --- a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs +++ b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs @@ -769,7 +769,7 @@ pub async fn handle_rpc( Ok(serde_json::to_value(response).unwrap()) } - ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1 => { + ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1 | ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V2 => { #[derive(Deserialize)] #[serde(transparent)] struct Quantity(#[serde(with = "serde_utils::u64_hex_be")] pub u64); @@ -810,7 +810,7 @@ pub async fn handle_rpc( }) .transpose() .unwrap(), - block_access_list: None, + block_access_list: payload.block_access_list().ok().cloned(), }; let json_payload_body: JsonExecutionPayloadBodyV1 = payload_body.try_into().unwrap(); diff --git a/beacon_node/execution_layer/src/test_utils/mod.rs b/beacon_node/execution_layer/src/test_utils/mod.rs index 2bdc0d11c07..9294990bfcb 100644 --- a/beacon_node/execution_layer/src/test_utils/mod.rs +++ b/beacon_node/execution_layer/src/test_utils/mod.rs @@ -51,6 +51,7 @@ pub const DEFAULT_ENGINE_CAPABILITIES: EngineCapabilities = EngineCapabilities { get_payload_bodies_by_hash_v1: true, get_payload_bodies_by_hash_v2: true, get_payload_bodies_by_range_v1: true, + get_payload_bodies_by_range_v2: true, get_payload_v1: true, get_payload_v2: true, get_payload_v3: true, From a20e38e127fd6e565ca259ae8a5d35e5f949d811 Mon Sep 17 00:00:00 2001 From: SamAg19 Date: Sun, 6 Sep 2026 03:26:32 +0530 Subject: [PATCH 3/9] Refactor engine_getPayloadBodiesByHashV* and engine_getPayloadBodiesByRangeV* --- .../execution_layer/src/engine_api/http.rs | 62 ++----------------- beacon_node/execution_layer/src/lib.rs | 31 ++++++++-- 2 files changed, 32 insertions(+), 61 deletions(-) diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index 3dfbdede8b9..f7db708a06f 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -1184,67 +1184,16 @@ impl HttpJsonRpc { Ok(response.into()) } - pub async fn get_payload_bodies_by_hash_v1( - &self, - block_hashes: Vec, - ) -> Result>>, Error> { - let params = json!([block_hashes]); - - let response: Vec>> = self - .rpc_request( - ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, - params, - ENGINE_GET_PAYLOAD_BODIES_TIMEOUT * self.execution_timeout_multiplier, - ) - .await?; - - response - .into_iter() - .map(|opt_json| { - opt_json - .map(|json| json.try_into().map_err(Error::from)) - .transpose() - }) - .collect::, _>>() - } - - pub async fn get_payload_bodies_by_hash_v2( + pub async fn get_payload_bodies_by_hash( &self, + method: &str, block_hashes: Vec, ) -> Result>>, Error> { let params = json!([block_hashes]); let response: Vec>> = self .rpc_request( - ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V2, - params, - ENGINE_GET_PAYLOAD_BODIES_TIMEOUT * self.execution_timeout_multiplier, - ) - .await?; - - response - .into_iter() - .map(|opt_json| { - opt_json - .map(|json| json.try_into().map_err(Error::from)) - .transpose() - }) - .collect::, _>>() - } - - pub async fn get_payload_bodies_by_range_v1( - &self, - start: u64, - count: u64, - ) -> Result>>, Error> { - #[derive(Serialize)] - #[serde(transparent)] - struct Quantity(#[serde(with = "serde_utils::u64_hex_be")] u64); - - let params = json!([Quantity(start), Quantity(count)]); - let response: Vec>> = self - .rpc_request( - ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, + method, params, ENGINE_GET_PAYLOAD_BODIES_TIMEOUT * self.execution_timeout_multiplier, ) @@ -1260,8 +1209,9 @@ impl HttpJsonRpc { .collect::, _>>() } - pub async fn get_payload_bodies_by_range_v2( + pub async fn get_payload_bodies_by_range( &self, + method: &str, start: u64, count: u64, ) -> Result>>, Error> { @@ -1272,7 +1222,7 @@ impl HttpJsonRpc { let params = json!([Quantity(start), Quantity(count)]); let response: Vec>> = self .rpc_request( - ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V2, + method, params, ENGINE_GET_PAYLOAD_BODIES_TIMEOUT * self.execution_timeout_multiplier, ) diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index e3adfd1542a..35ba57411e4 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -14,7 +14,14 @@ use builder_client::PreGloasBuilderHttpClient; pub use engine_api::EngineCapabilities; use engine_api::Error as ApiError; pub use engine_api::*; -pub use engine_api::{http, http::HttpJsonRpc, http::deposit_methods}; +pub use engine_api::{ + http, + http::deposit_methods, + http::{ + ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V2, + ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V2, HttpJsonRpc, + }, +}; use engines::{Engine, EngineError}; pub use engines::{EngineState, ForkchoiceState}; use eth2::types::{BlobsBundle, FullPayloadContents}; @@ -1669,9 +1676,15 @@ impl ExecutionLayer { self.engine() .request(|engine| async move { if capabilities.get_payload_bodies_by_hash_v2 { - engine.api.get_payload_bodies_by_hash_v2(hashes).await + engine + .api + .get_payload_bodies_by_hash(ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V2, hashes) + .await } else { - engine.api.get_payload_bodies_by_hash_v1(hashes).await + engine + .api + .get_payload_bodies_by_hash(ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, hashes) + .await } }) .await @@ -1691,12 +1704,20 @@ impl ExecutionLayer { if capabilities.get_payload_bodies_by_range_v2 { engine .api - .get_payload_bodies_by_range_v2(start, count) + .get_payload_bodies_by_range( + ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V2, + start, + count, + ) .await } else { engine .api - .get_payload_bodies_by_range_v1(start, count) + .get_payload_bodies_by_range( + ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, + start, + count, + ) .await } }) From a98ee83f5342cfd70ba70ff4268fd543a107f327 Mon Sep 17 00:00:00 2001 From: SamAg19 Date: Mon, 7 Sep 2026 01:29:06 +0530 Subject: [PATCH 4/9] Error out when neither V1 nor V2 are advertised by EL --- beacon_node/execution_layer/src/lib.rs | 59 ++++++++++++-------------- 1 file changed, 26 insertions(+), 33 deletions(-) diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index 35ba57411e4..ea734d39ab5 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -161,6 +161,7 @@ pub enum Error { transactions_root: Hash256, }, ZeroLengthTransaction, + PayloadBodiesByHashNotSupported, PayloadBodiesByRangeNotSupported, GetBlobsNotSupported, GetInclusionListNotSupported, @@ -1673,20 +1674,18 @@ impl ExecutionLayer { hashes: Vec, ) -> Result>>, Error> { let capabilities = self.get_engine_capabilities(None).await?; + let method = if capabilities.get_payload_bodies_by_hash_v2 { + ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V2 + } else if capabilities.get_payload_bodies_by_hash_v1 { + ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1 + } else { + return Err(Error::PayloadBodiesByHashNotSupported); + }; + self.engine() - .request(|engine| async move { - if capabilities.get_payload_bodies_by_hash_v2 { - engine - .api - .get_payload_bodies_by_hash(ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V2, hashes) - .await - } else { - engine - .api - .get_payload_bodies_by_hash(ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, hashes) - .await - } - }) + .request( + |engine| async move { engine.api.get_payload_bodies_by_hash(method, hashes).await }, + ) .await .map_err(Box::new) .map_err(Error::EngineError) @@ -1699,27 +1698,20 @@ impl ExecutionLayer { ) -> Result>>, Error> { let capabilities = self.get_engine_capabilities(None).await?; let _timer = metrics::start_timer(&metrics::EXECUTION_LAYER_GET_PAYLOAD_BODIES_BY_RANGE); + let method = if capabilities.get_payload_bodies_by_range_v2 { + ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V2 + } else if capabilities.get_payload_bodies_by_range_v1 { + ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1 + } else { + return Err(Error::PayloadBodiesByRangeNotSupported); + }; + self.engine() .request(|engine| async move { - if capabilities.get_payload_bodies_by_range_v2 { - engine - .api - .get_payload_bodies_by_range( - ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V2, - start, - count, - ) - .await - } else { - engine - .api - .get_payload_bodies_by_range( - ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, - start, - count, - ) - .await - } + engine + .api + .get_payload_bodies_by_range(method, start, count) + .await }) .await .map_err(Box::new) @@ -1759,7 +1751,8 @@ impl ExecutionLayer { // Use efficient payload bodies by range method if supported. let capabilities = self.get_engine_capabilities(None).await?; - if capabilities.get_payload_bodies_by_range_v1 { + if capabilities.get_payload_bodies_by_range_v1 | capabilities.get_payload_bodies_by_range_v2 + { let mut payload_bodies = self.get_payload_bodies_by_range(block_number, 1).await?; if payload_bodies.len() != 1 { From 9feccf4a7a1ee3c89fe2c83f2d73a861f067741e Mon Sep 17 00:00:00 2001 From: hopinheimer Date: Sun, 6 Sep 2026 21:10:59 -0700 Subject: [PATCH 5/9] Use dedicated types for V2 execution payload bodies. --- beacon_node/execution_layer/src/engine_api.rs | 10 +- .../execution_layer/src/engine_api/http.rs | 54 +++++++- .../src/engine_api/json_structures.rs | 48 +++++-- beacon_node/execution_layer/src/lib.rs | 127 +++++++++++++----- .../src/test_utils/handle_rpc.rs | 91 ++++++++++++- 5 files changed, 277 insertions(+), 53 deletions(-) diff --git a/beacon_node/execution_layer/src/engine_api.rs b/beacon_node/execution_layer/src/engine_api.rs index 3aefc3ff673..f306d3e54cd 100644 --- a/beacon_node/execution_layer/src/engine_api.rs +++ b/beacon_node/execution_layer/src/engine_api.rs @@ -28,7 +28,7 @@ pub use types::{ use types::{ ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadDeneb, ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionPayloadGloas, ExecutionPayloadHeze, - ExecutionRequests, KzgProofs, + ExecutionRequests, KzgProofs, ProgressiveTransactions, ProgressiveWithdrawals, }; use types::{GRAFFITI_BYTES_LEN, Graffiti}; @@ -448,6 +448,14 @@ impl GetPayloadResponse { pub struct ExecutionPayloadBodyV1 { pub transactions: Transactions, pub withdrawals: Option>, +} + +/// The execution payload body returned by `engine_getPayloadBodiesByHashV2` and +/// `engine_getPayloadBodiesByRangeV2`. +#[derive(Clone, Debug, PartialEq)] +pub struct ExecutionPayloadBodyV2 { + pub transactions: ProgressiveTransactions, + pub withdrawals: Option, pub block_access_list: Option, } diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index f7db708a06f..08ff0c5206e 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -1184,16 +1184,15 @@ impl HttpJsonRpc { Ok(response.into()) } - pub async fn get_payload_bodies_by_hash( + pub async fn get_payload_bodies_by_hash_v1( &self, - method: &str, block_hashes: Vec, ) -> Result>>, Error> { let params = json!([block_hashes]); let response: Vec>> = self .rpc_request( - method, + ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, params, ENGINE_GET_PAYLOAD_BODIES_TIMEOUT * self.execution_timeout_multiplier, ) @@ -1209,9 +1208,28 @@ impl HttpJsonRpc { .collect::, _>>() } - pub async fn get_payload_bodies_by_range( + pub async fn get_payload_bodies_by_hash_v2( + &self, + block_hashes: Vec, + ) -> Result>, Error> { + let params = json!([block_hashes]); + + let response: Vec> = self + .rpc_request( + ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V2, + params, + ENGINE_GET_PAYLOAD_BODIES_TIMEOUT * self.execution_timeout_multiplier, + ) + .await?; + + Ok(response + .into_iter() + .map(|body| body.map(Into::into)) + .collect()) + } + + pub async fn get_payload_bodies_by_range_v1( &self, - method: &str, start: u64, count: u64, ) -> Result>>, Error> { @@ -1222,7 +1240,7 @@ impl HttpJsonRpc { let params = json!([Quantity(start), Quantity(count)]); let response: Vec>> = self .rpc_request( - method, + ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, params, ENGINE_GET_PAYLOAD_BODIES_TIMEOUT * self.execution_timeout_multiplier, ) @@ -1238,6 +1256,30 @@ impl HttpJsonRpc { .collect::, _>>() } + pub async fn get_payload_bodies_by_range_v2( + &self, + start: u64, + count: u64, + ) -> Result>, Error> { + #[derive(Serialize)] + #[serde(transparent)] + struct Quantity(#[serde(with = "serde_utils::u64_hex_be")] u64); + + let params = json!([Quantity(start), Quantity(count)]); + let response: Vec> = self + .rpc_request( + ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V2, + params, + ENGINE_GET_PAYLOAD_BODIES_TIMEOUT * self.execution_timeout_multiplier, + ) + .await?; + + Ok(response + .into_iter() + .map(|body| body.map(Into::into)) + .collect()) + } + pub async fn exchange_capabilities(&self) -> Result { let params = json!([LIGHTHOUSE_CAPABILITIES]); diff --git a/beacon_node/execution_layer/src/engine_api/json_structures.rs b/beacon_node/execution_layer/src/engine_api/json_structures.rs index 3a1ad2a77fe..8c87f4929bc 100644 --- a/beacon_node/execution_layer/src/engine_api/json_structures.rs +++ b/beacon_node/execution_layer/src/engine_api/json_structures.rs @@ -1214,15 +1214,47 @@ pub struct JsonBlockAccessList( ); #[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(bound = "E: EthSpec", rename_all = "camelCase")] +#[serde(bound = "E: EthSpec")] pub struct JsonExecutionPayloadBodyV1 { #[serde(with = "ssz_types::serde_utils::list_of_hex_var_list")] pub transactions: Transactions, pub withdrawals: Option>, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JsonExecutionPayloadBodyV2 { + #[serde(with = "ssz_types::serde_utils::prog_list_of_hex_prog_var_list")] + pub transactions: ProgressiveTransactions, + pub withdrawals: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub block_access_list: Option, } +impl From for ExecutionPayloadBodyV2 { + fn from(value: JsonExecutionPayloadBodyV2) -> Self { + Self { + transactions: value.transactions, + withdrawals: value + .withdrawals + .map(|withdrawals| withdrawals.into_iter().map(Into::into).collect()), + block_access_list: value.block_access_list.map(|list| list.0), + } + } +} + +impl From for JsonExecutionPayloadBodyV2 { + fn from(value: ExecutionPayloadBodyV2) -> Self { + Self { + transactions: value.transactions, + withdrawals: value + .withdrawals + .map(|withdrawals| withdrawals.into_iter().map(Into::into).collect()), + block_access_list: value.block_access_list.map(JsonBlockAccessList), + } + } +} + impl TryFrom> for ExecutionPayloadBodyV1 { type Error = ssz_types::Error; @@ -1230,7 +1262,6 @@ impl TryFrom> for ExecutionPayloadBody Ok(Self { transactions: value.transactions, withdrawals: value.withdrawals.map(withdrawals_from_json).transpose()?, - block_access_list: value.block_access_list.map(|list| list.0), }) } } @@ -1242,7 +1273,6 @@ impl TryFrom> for JsonExecutionPayloadBody Ok(Self { transactions: value.transactions, withdrawals: value.withdrawals.map(withdrawals_to_json).transpose()?, - block_access_list: value.block_access_list.map(JsonBlockAccessList), }) } } @@ -1672,9 +1702,8 @@ mod tests { "withdrawals": null, "blockAccessList": "0x010203", }); - let body: JsonExecutionPayloadBodyV1 = - serde_json::from_value(with_bal.clone()).unwrap(); - let internal: ExecutionPayloadBodyV1 = body.clone().try_into().unwrap(); + let body: JsonExecutionPayloadBodyV2 = serde_json::from_value(with_bal.clone()).unwrap(); + let internal: ExecutionPayloadBodyV2 = body.clone().into(); assert_eq!( internal.block_access_list, Some(ProgressiveVariableList::new(vec![1, 2, 3])) @@ -1687,9 +1716,8 @@ mod tests { "withdrawals": null, "blockAccessList": null, }); - let body: JsonExecutionPayloadBodyV1 = - serde_json::from_value(null_bal).unwrap(); - let internal: ExecutionPayloadBodyV1 = body.clone().try_into().unwrap(); + let body: JsonExecutionPayloadBodyV2 = serde_json::from_value(null_bal).unwrap(); + let internal: ExecutionPayloadBodyV2 = body.clone().into(); assert_eq!(internal.block_access_list, None); assert_eq!( serde_json::to_value(&body).unwrap(), @@ -1697,7 +1725,7 @@ mod tests { ); // Omitted -> `None`. - let body: JsonExecutionPayloadBodyV1 = + let body: JsonExecutionPayloadBodyV2 = serde_json::from_value(json!({ "transactions": [], "withdrawals": null })).unwrap(); assert!(body.block_access_list.is_none()); } diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index ea734d39ab5..f8bc689cb2c 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -14,14 +14,7 @@ use builder_client::PreGloasBuilderHttpClient; pub use engine_api::EngineCapabilities; use engine_api::Error as ApiError; pub use engine_api::*; -pub use engine_api::{ - http, - http::deposit_methods, - http::{ - ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V2, - ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V2, HttpJsonRpc, - }, -}; +pub use engine_api::{http, http::HttpJsonRpc, http::deposit_methods}; use engines::{Engine, EngineError}; pub use engines::{EngineState, ForkchoiceState}; use eth2::types::{BlobsBundle, FullPayloadContents}; @@ -161,8 +154,9 @@ pub enum Error { transactions_root: Hash256, }, ZeroLengthTransaction, - PayloadBodiesByHashNotSupported, + PayloadBodiesByHashV2NotSupported, PayloadBodiesByRangeNotSupported, + PayloadBodiesByRangeV2NotSupported, GetBlobsNotSupported, GetInclusionListNotSupported, InvalidJWTSecret(String), @@ -1673,19 +1667,29 @@ impl ExecutionLayer { &self, hashes: Vec, ) -> Result>>, Error> { + self.engine() + .request(|engine: &Engine| async move { + engine.api.get_payload_bodies_by_hash_v1(hashes).await + }) + .await + .map_err(Box::new) + .map_err(Error::EngineError) + } + + /// Fetch execution payload bodies using the Gloas V2 response format. + pub async fn get_payload_bodies_by_hash_v2( + &self, + hashes: Vec, + ) -> Result>, Error> { let capabilities = self.get_engine_capabilities(None).await?; - let method = if capabilities.get_payload_bodies_by_hash_v2 { - ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V2 - } else if capabilities.get_payload_bodies_by_hash_v1 { - ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1 - } else { - return Err(Error::PayloadBodiesByHashNotSupported); - }; + if !capabilities.get_payload_bodies_by_hash_v2 { + return Err(Error::PayloadBodiesByHashV2NotSupported); + } self.engine() - .request( - |engine| async move { engine.api.get_payload_bodies_by_hash(method, hashes).await }, - ) + .request(|engine: &Engine| async move { + engine.api.get_payload_bodies_by_hash_v2(hashes).await + }) .await .map_err(Box::new) .map_err(Error::EngineError) @@ -1696,21 +1700,36 @@ impl ExecutionLayer { start: u64, count: u64, ) -> Result>>, Error> { - let capabilities = self.get_engine_capabilities(None).await?; let _timer = metrics::start_timer(&metrics::EXECUTION_LAYER_GET_PAYLOAD_BODIES_BY_RANGE); - let method = if capabilities.get_payload_bodies_by_range_v2 { - ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V2 - } else if capabilities.get_payload_bodies_by_range_v1 { - ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1 - } else { - return Err(Error::PayloadBodiesByRangeNotSupported); - }; + self.engine() + .request(|engine: &Engine| async move { + engine + .api + .get_payload_bodies_by_range_v1(start, count) + .await + }) + .await + .map_err(Box::new) + .map_err(Error::EngineError) + } + + /// Fetch execution payload bodies by range using the Gloas V2 response format. + pub async fn get_payload_bodies_by_range_v2( + &self, + start: u64, + count: u64, + ) -> Result>, Error> { + let capabilities = self.get_engine_capabilities(None).await?; + if !capabilities.get_payload_bodies_by_range_v2 { + return Err(Error::PayloadBodiesByRangeV2NotSupported); + } + let _timer = metrics::start_timer(&metrics::EXECUTION_LAYER_GET_PAYLOAD_BODIES_BY_RANGE); self.engine() - .request(|engine| async move { + .request(|engine: &Engine| async move { engine .api - .get_payload_bodies_by_range(method, start, count) + .get_payload_bodies_by_range_v2(start, count) .await }) .await @@ -1751,8 +1770,7 @@ impl ExecutionLayer { // Use efficient payload bodies by range method if supported. let capabilities = self.get_engine_capabilities(None).await?; - if capabilities.get_payload_bodies_by_range_v1 | capabilities.get_payload_bodies_by_range_v2 - { + if capabilities.get_payload_bodies_by_range_v1 { let mut payload_bodies = self.get_payload_bodies_by_range(block_number, 1).await?; if payload_bodies.len() != 1 { @@ -2202,7 +2220,7 @@ fn noop( #[cfg(test)] mod test { use super::*; - use crate::test_utils::MockExecutionLayer as GenericMockExecutionLayer; + use crate::test_utils::{Block, MockExecutionLayer as GenericMockExecutionLayer}; use task_executor::test_utils::TestRuntime; use types::MainnetEthSpec; @@ -2220,6 +2238,51 @@ mod test { .await; } + #[tokio::test] + async fn get_gloas_payload_bodies_v2() { + let runtime = TestRuntime::default(); + let mock = MockExecutionLayer::default_params(runtime.task_executor.clone()); + let block_hash = ExecutionBlockHash::repeat_byte(0x42); + let block_number = 42; + let payload = ExecutionPayloadGloas { + block_hash, + block_number, + ..Default::default() + }; + let expected_body = ExecutionPayloadBodyV2 { + transactions: payload.transactions.clone(), + withdrawals: Some(payload.withdrawals.clone()), + block_access_list: Some(payload.block_access_list.clone()), + }; + let mut block_generator = mock.server.execution_block_generator(); + block_generator.insert_block_without_checks(Block::PoS(payload.into())); + block_generator + .forkchoice_updated( + ForkchoiceState { + head_block_hash: block_hash, + safe_block_hash: block_hash, + finalized_block_hash: block_hash, + }, + None, + ) + .expect("block should become the mock execution head"); + drop(block_generator); + + let bodies_by_hash = mock + .el + .get_payload_bodies_by_hash_v2(vec![block_hash, ExecutionBlockHash::zero()]) + .await + .expect("payload body request by hash should succeed"); + assert_eq!(bodies_by_hash, vec![Some(expected_body.clone()), None]); + + let bodies_by_range = mock + .el + .get_payload_bodies_by_range_v2(block_number, 2) + .await + .expect("payload body request by range should succeed"); + assert_eq!(bodies_by_range, vec![Some(expected_body), None]); + } + #[tokio::test] async fn test_expected_gas_limit() { let spec = ChainSpec::mainnet(); diff --git a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs index 16ad14175c1..818781f7332 100644 --- a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs +++ b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, de::DeserializeOwned}; use serde_json::Value as JsonValue; use std::sync::Arc; use tracing::debug; +use types::{ProgressiveTransactions, ProgressiveWithdrawals}; pub const GENERIC_ERROR_CODE: i64 = -1234; pub const BAD_PARAMS_ERROR_CODE: i64 = -32602; @@ -724,7 +725,7 @@ pub async fn handle_rpc( ENGINE_GET_CLIENT_VERSION_V1 => { Ok(serde_json::to_value([DEFAULT_CLIENT_VERSION.clone()]).unwrap()) } - ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1 | ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V2 => { + ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1 => { let block_hashes = get_param::>(params, 0) .map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?; @@ -757,7 +758,6 @@ pub async fn handle_rpc( }) .transpose() .unwrap(), - block_access_list: payload.block_access_list().ok().cloned(), }; let json_payload_body: JsonExecutionPayloadBodyV1 = payload_body.try_into().unwrap(); @@ -769,7 +769,45 @@ pub async fn handle_rpc( Ok(serde_json::to_value(response).unwrap()) } - ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1 | ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V2 => { + ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V2 => { + let block_hashes = get_param::>(params, 0) + .map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?; + + let mut response = vec![]; + for block_hash in block_hashes { + let maybe_payload = ctx + .execution_block_generator + .read() + .execution_payload_by_hash(block_hash); + + match maybe_payload { + Some(payload) => { + let payload_body = ExecutionPayloadBodyV2 { + transactions: ProgressiveTransactions::new( + payload + .transactions() + .iter() + .map(|transaction| { + ssz_types::ProgressiveVariableList::new( + transaction.to_vec(), + ) + }) + .collect(), + ), + withdrawals: payload.withdrawals().ok().map(|withdrawals| { + ProgressiveWithdrawals::new(withdrawals.to_vec()) + }), + block_access_list: payload.block_access_list().ok().cloned(), + }; + response.push(Some(JsonExecutionPayloadBodyV2::from(payload_body))); + } + None => response.push(None), + } + } + + Ok(serde_json::to_value(response).unwrap()) + } + ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1 => { #[derive(Deserialize)] #[serde(transparent)] struct Quantity(#[serde(with = "serde_utils::u64_hex_be")] pub u64); @@ -810,7 +848,6 @@ pub async fn handle_rpc( }) .transpose() .unwrap(), - block_access_list: payload.block_access_list().ok().cloned(), }; let json_payload_body: JsonExecutionPayloadBodyV1 = payload_body.try_into().unwrap(); @@ -822,6 +859,52 @@ pub async fn handle_rpc( Ok(serde_json::to_value(response).unwrap()) } + ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V2 => { + #[derive(Deserialize)] + #[serde(transparent)] + struct Quantity(#[serde(with = "serde_utils::u64_hex_be")] pub u64); + + let start = get_param::(params, 0) + .map_err(|s| (s, BAD_PARAMS_ERROR_CODE))? + .0; + let count = get_param::(params, 1) + .map_err(|s| (s, BAD_PARAMS_ERROR_CODE))? + .0; + + let mut response = vec![]; + for block_num in start..(start + count) { + let maybe_payload = ctx + .execution_block_generator + .read() + .execution_payload_by_number(block_num); + + match maybe_payload { + Some(payload) => { + let payload_body = ExecutionPayloadBodyV2 { + transactions: ProgressiveTransactions::new( + payload + .transactions() + .iter() + .map(|transaction| { + ssz_types::ProgressiveVariableList::new( + transaction.to_vec(), + ) + }) + .collect(), + ), + withdrawals: payload.withdrawals().ok().map(|withdrawals| { + ProgressiveWithdrawals::new(withdrawals.to_vec()) + }), + block_access_list: payload.block_access_list().ok().cloned(), + }; + response.push(Some(JsonExecutionPayloadBodyV2::from(payload_body))); + } + None => response.push(None), + } + } + + Ok(serde_json::to_value(response).unwrap()) + } other => Err(( format!("The method {} does not exist/is not available", other), METHOD_NOT_FOUND_CODE, From 4a8ade1371b832faa06a2a231b7ba19dfb6687ee Mon Sep 17 00:00:00 2001 From: hopinheimer Date: Mon, 7 Sep 2026 00:22:01 -0700 Subject: [PATCH 6/9] Fix V2 payload body serialization --- .../src/engine_api/json_structures.rs | 21 ++++++++++++------- beacon_node/execution_layer/src/lib.rs | 10 +++++++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/beacon_node/execution_layer/src/engine_api/json_structures.rs b/beacon_node/execution_layer/src/engine_api/json_structures.rs index 8c87f4929bc..20bbe8a6434 100644 --- a/beacon_node/execution_layer/src/engine_api/json_structures.rs +++ b/beacon_node/execution_layer/src/engine_api/json_structures.rs @@ -1227,7 +1227,7 @@ pub struct JsonExecutionPayloadBodyV2 { #[serde(with = "ssz_types::serde_utils::prog_list_of_hex_prog_var_list")] pub transactions: ProgressiveTransactions, pub withdrawals: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] + #[serde(default)] pub block_access_list: Option, } @@ -1710,23 +1710,28 @@ mod tests { ); assert_eq!(serde_json::to_value(&body).unwrap(), with_bal); - // Explicit `null` -> `None`, omitted on re-serialize. + // Explicit `null` -> `None`, retained as `null` on re-serialize. let null_bal = json!({ "transactions": [], "withdrawals": null, "blockAccessList": null, }); - let body: JsonExecutionPayloadBodyV2 = serde_json::from_value(null_bal).unwrap(); + let body: JsonExecutionPayloadBodyV2 = serde_json::from_value(null_bal.clone()).unwrap(); let internal: ExecutionPayloadBodyV2 = body.clone().into(); assert_eq!(internal.block_access_list, None); - assert_eq!( - serde_json::to_value(&body).unwrap(), - json!({ "transactions": [], "withdrawals": null }) - ); + assert_eq!(serde_json::to_value(&body).unwrap(), null_bal); - // Omitted -> `None`. + // An omitted field is accepted as `None`, then serialized in its canonical `null` form. let body: JsonExecutionPayloadBodyV2 = serde_json::from_value(json!({ "transactions": [], "withdrawals": null })).unwrap(); assert!(body.block_access_list.is_none()); + assert_eq!( + serde_json::to_value(&body).unwrap(), + json!({ + "transactions": [], + "withdrawals": null, + "blockAccessList": null, + }) + ); } } diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index f8bc689cb2c..124171d888d 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -2247,6 +2247,16 @@ mod test { let payload = ExecutionPayloadGloas { block_hash, block_number, + transactions: ProgressiveTransactions::new(vec![ + ssz_types::ProgressiveVariableList::new(vec![0x01, 0x02, 0x03]), + ]), + withdrawals: types::ProgressiveWithdrawals::new(vec![Withdrawal { + index: 1, + validator_index: 2, + address: Address::from([0x33; 20]), + amount: 3, + }]), + block_access_list: BlockAccessList::new(vec![0x04, 0x05, 0x06]), ..Default::default() }; let expected_body = ExecutionPayloadBodyV2 { From 156db75a983041e735d2cb55691f97e79faf136d Mon Sep 17 00:00:00 2001 From: SamAg19 Date: Mon, 7 Sep 2026 13:11:31 +0530 Subject: [PATCH 7/9] clippy fixes --- beacon_node/execution_layer/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index 124171d888d..33b9c79df6d 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -2239,6 +2239,7 @@ mod test { } #[tokio::test] + #[allow(clippy::await_holding_lock)] // This is a test, so it should be fine. async fn get_gloas_payload_bodies_v2() { let runtime = TestRuntime::default(); let mock = MockExecutionLayer::default_params(runtime.task_executor.clone()); From 4545efa37c6315d2aef5bf3bd2b332efbc350ffe Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Tue, 8 Sep 2026 11:51:27 +1000 Subject: [PATCH 8/9] Delete payloadsByRangeV2 --- .../execution_layer/src/engine_api/http.rs | 26 ++-------------- beacon_node/execution_layer/src/lib.rs | 31 ------------------- 2 files changed, 2 insertions(+), 55 deletions(-) diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index 08ff0c5206e..e83a9c3d8f9 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -79,6 +79,8 @@ pub const EIP155_ERROR_STR: &str = "chain not synced beyond EIP-155 replay-prote /// (verified geth, nethermind, erigon, besu) pub const METHOD_NOT_FOUND_CODE: i64 = -32601; +// We never actually use getPayloadBodiesByRangeV2, but it seems harmless to include it in +// our supported capabilities to avoid scaring the EL. pub static LIGHTHOUSE_CAPABILITIES: &[&str] = &[ ENGINE_NEW_PAYLOAD_V1, ENGINE_NEW_PAYLOAD_V2, @@ -1256,30 +1258,6 @@ impl HttpJsonRpc { .collect::, _>>() } - pub async fn get_payload_bodies_by_range_v2( - &self, - start: u64, - count: u64, - ) -> Result>, Error> { - #[derive(Serialize)] - #[serde(transparent)] - struct Quantity(#[serde(with = "serde_utils::u64_hex_be")] u64); - - let params = json!([Quantity(start), Quantity(count)]); - let response: Vec> = self - .rpc_request( - ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V2, - params, - ENGINE_GET_PAYLOAD_BODIES_TIMEOUT * self.execution_timeout_multiplier, - ) - .await?; - - Ok(response - .into_iter() - .map(|body| body.map(Into::into)) - .collect()) - } - pub async fn exchange_capabilities(&self) -> Result { let params = json!([LIGHTHOUSE_CAPABILITIES]); diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index 33b9c79df6d..59e2a381531 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -1713,30 +1713,6 @@ impl ExecutionLayer { .map_err(Error::EngineError) } - /// Fetch execution payload bodies by range using the Gloas V2 response format. - pub async fn get_payload_bodies_by_range_v2( - &self, - start: u64, - count: u64, - ) -> Result>, Error> { - let capabilities = self.get_engine_capabilities(None).await?; - if !capabilities.get_payload_bodies_by_range_v2 { - return Err(Error::PayloadBodiesByRangeV2NotSupported); - } - - let _timer = metrics::start_timer(&metrics::EXECUTION_LAYER_GET_PAYLOAD_BODIES_BY_RANGE); - self.engine() - .request(|engine: &Engine| async move { - engine - .api - .get_payload_bodies_by_range_v2(start, count) - .await - }) - .await - .map_err(Box::new) - .map_err(Error::EngineError) - } - /// Fetch a full payload from the execution node. /// /// This will fail if the payload is not from the finalized portion of the chain. @@ -2285,13 +2261,6 @@ mod test { .await .expect("payload body request by hash should succeed"); assert_eq!(bodies_by_hash, vec![Some(expected_body.clone()), None]); - - let bodies_by_range = mock - .el - .get_payload_bodies_by_range_v2(block_number, 2) - .await - .expect("payload body request by range should succeed"); - assert_eq!(bodies_by_range, vec![Some(expected_body), None]); } #[tokio::test] From ad8a559ac4b35f8d0ca78caf5b26f8ef93fa1ef3 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Tue, 8 Sep 2026 12:00:38 +1000 Subject: [PATCH 9/9] Delete more --- beacon_node/execution_layer/src/lib.rs | 1 - .../src/test_utils/handle_rpc.rs | 46 ------------------- 2 files changed, 47 deletions(-) diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index 59e2a381531..771cdfa86a9 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -156,7 +156,6 @@ pub enum Error { ZeroLengthTransaction, PayloadBodiesByHashV2NotSupported, PayloadBodiesByRangeNotSupported, - PayloadBodiesByRangeV2NotSupported, GetBlobsNotSupported, GetInclusionListNotSupported, InvalidJWTSecret(String), diff --git a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs index 818781f7332..8dd08a9223d 100644 --- a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs +++ b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs @@ -859,52 +859,6 @@ pub async fn handle_rpc( Ok(serde_json::to_value(response).unwrap()) } - ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V2 => { - #[derive(Deserialize)] - #[serde(transparent)] - struct Quantity(#[serde(with = "serde_utils::u64_hex_be")] pub u64); - - let start = get_param::(params, 0) - .map_err(|s| (s, BAD_PARAMS_ERROR_CODE))? - .0; - let count = get_param::(params, 1) - .map_err(|s| (s, BAD_PARAMS_ERROR_CODE))? - .0; - - let mut response = vec![]; - for block_num in start..(start + count) { - let maybe_payload = ctx - .execution_block_generator - .read() - .execution_payload_by_number(block_num); - - match maybe_payload { - Some(payload) => { - let payload_body = ExecutionPayloadBodyV2 { - transactions: ProgressiveTransactions::new( - payload - .transactions() - .iter() - .map(|transaction| { - ssz_types::ProgressiveVariableList::new( - transaction.to_vec(), - ) - }) - .collect(), - ), - withdrawals: payload.withdrawals().ok().map(|withdrawals| { - ProgressiveWithdrawals::new(withdrawals.to_vec()) - }), - block_access_list: payload.block_access_list().ok().cloned(), - }; - response.push(Some(JsonExecutionPayloadBodyV2::from(payload_body))); - } - None => response.push(None), - } - } - - Ok(serde_json::to_value(response).unwrap()) - } other => Err(( format!("The method {} does not exist/is not available", other), METHOD_NOT_FOUND_CODE,