diff --git a/beacon_node/beacon_chain/src/beacon_block_streamer.rs b/beacon_node/beacon_chain/src/beacon_block_streamer.rs index a462376cc0..f499f6fb84 100644 --- a/beacon_node/beacon_chain/src/beacon_block_streamer.rs +++ b/beacon_node/beacon_chain/src/beacon_block_streamer.rs @@ -1,5 +1,5 @@ use crate::{BeaconChain, BeaconChainError, BeaconChainTypes, BlockProcessStatus, metrics}; -use execution_layer::{ExecutionLayer, ExecutionPayloadBodyV1}; +use execution_layer::{ExecutionLayer, ExecutionPayloadBody}; use logging::crit; use std::collections::HashMap; use std::sync::Arc; @@ -58,7 +58,7 @@ struct BodiesByRange { struct BlockParts { blinded_block: Box>, header: Box>, - body: Option>>, + body: Option>>, } impl BlockParts { @@ -634,7 +634,9 @@ impl BeaconBlockStreamer { .map_err(BeaconChainError::EngineGetCapabilititesFailed) { Ok(engine_capabilities) => { - if engine_capabilities.get_payload_bodies_by_range_v1 { + if engine_capabilities.get_payload_bodies_by_range_v2 + || engine_capabilities.get_payload_bodies_by_range_v1 + { self.stream_blocks(block_roots, sender).await; } else { // use the fallback method diff --git a/beacon_node/execution_layer/src/engine_api.rs b/beacon_node/execution_layer/src/engine_api.rs index f0238f8f98..df49b7b271 100644 --- a/beacon_node/execution_layer/src/engine_api.rs +++ b/beacon_node/execution_layer/src/engine_api.rs @@ -3,7 +3,8 @@ use crate::http::{ ENGINE_FORKCHOICE_UPDATED_V1, ENGINE_FORKCHOICE_UPDATED_V2, ENGINE_FORKCHOICE_UPDATED_V3, ENGINE_FORKCHOICE_UPDATED_V4, ENGINE_GET_BLOBS_V1, ENGINE_GET_BLOBS_V2, ENGINE_GET_CLIENT_VERSION_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_BODIES_BY_HASH_V2, ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, + 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, @@ -425,10 +426,18 @@ impl GetPayloadResponse { } } +#[superstruct( + variants(V1, V2), + variant_attributes(derive(Clone, Debug)), + cast_error(ty = "Error", expr = "Error::IncorrectStateVariant"), + partial_getter_error(ty = "Error", expr = "Error::IncorrectStateVariant") +)] #[derive(Clone, Debug)] -pub struct ExecutionPayloadBodyV1 { +pub struct ExecutionPayloadBody { pub transactions: Transactions, pub withdrawals: Option>, + #[superstruct(only(V2))] + pub block_access_list: Option>, } impl ExecutionPayloadBodyV1 { @@ -608,6 +617,195 @@ impl ExecutionPayloadBodyV1 { } } +impl ExecutionPayloadBodyV2 { + pub fn to_payload( + self, + header: ExecutionPayloadHeader, + ) -> Result, String> { + match header { + ExecutionPayloadHeader::Bellatrix(header) => { + if self.withdrawals.is_some() { + return Err(format!( + "block {} is bellatrix but payload body has withdrawals", + header.block_hash + )); + } + Ok(ExecutionPayload::Bellatrix(ExecutionPayloadBellatrix { + parent_hash: header.parent_hash, + fee_recipient: header.fee_recipient, + state_root: header.state_root, + receipts_root: header.receipts_root, + logs_bloom: header.logs_bloom, + prev_randao: header.prev_randao, + block_number: header.block_number, + gas_limit: header.gas_limit, + gas_used: header.gas_used, + timestamp: header.timestamp, + extra_data: header.extra_data, + base_fee_per_gas: header.base_fee_per_gas, + block_hash: header.block_hash, + transactions: self.transactions, + })) + } + ExecutionPayloadHeader::Capella(header) => { + if let Some(withdrawals) = self.withdrawals { + Ok(ExecutionPayload::Capella(ExecutionPayloadCapella { + parent_hash: header.parent_hash, + fee_recipient: header.fee_recipient, + state_root: header.state_root, + receipts_root: header.receipts_root, + logs_bloom: header.logs_bloom, + prev_randao: header.prev_randao, + block_number: header.block_number, + gas_limit: header.gas_limit, + gas_used: header.gas_used, + timestamp: header.timestamp, + extra_data: header.extra_data, + base_fee_per_gas: header.base_fee_per_gas, + block_hash: header.block_hash, + transactions: self.transactions, + withdrawals, + })) + } else { + Err(format!( + "block {} is capella but payload body doesn't have withdrawals", + header.block_hash + )) + } + } + ExecutionPayloadHeader::Deneb(header) => { + if let Some(withdrawals) = self.withdrawals { + Ok(ExecutionPayload::Deneb(ExecutionPayloadDeneb { + parent_hash: header.parent_hash, + fee_recipient: header.fee_recipient, + state_root: header.state_root, + receipts_root: header.receipts_root, + logs_bloom: header.logs_bloom, + prev_randao: header.prev_randao, + block_number: header.block_number, + gas_limit: header.gas_limit, + gas_used: header.gas_used, + timestamp: header.timestamp, + extra_data: header.extra_data, + base_fee_per_gas: header.base_fee_per_gas, + block_hash: header.block_hash, + transactions: self.transactions, + withdrawals, + blob_gas_used: header.blob_gas_used, + excess_blob_gas: header.excess_blob_gas, + })) + } else { + Err(format!( + "block {} is post capella but payload body doesn't have withdrawals", + header.block_hash + )) + } + } + ExecutionPayloadHeader::Electra(header) => { + if let Some(withdrawals) = self.withdrawals { + Ok(ExecutionPayload::Electra(ExecutionPayloadElectra { + parent_hash: header.parent_hash, + fee_recipient: header.fee_recipient, + state_root: header.state_root, + receipts_root: header.receipts_root, + logs_bloom: header.logs_bloom, + prev_randao: header.prev_randao, + block_number: header.block_number, + gas_limit: header.gas_limit, + gas_used: header.gas_used, + timestamp: header.timestamp, + extra_data: header.extra_data, + base_fee_per_gas: header.base_fee_per_gas, + block_hash: header.block_hash, + transactions: self.transactions, + withdrawals, + blob_gas_used: header.blob_gas_used, + excess_blob_gas: header.excess_blob_gas, + })) + } else { + Err(format!( + "block {} is post capella but payload body doesn't have withdrawals", + header.block_hash + )) + } + } + ExecutionPayloadHeader::Fulu(header) => { + if let Some(withdrawals) = self.withdrawals { + Ok(ExecutionPayload::Fulu(ExecutionPayloadFulu { + parent_hash: header.parent_hash, + fee_recipient: header.fee_recipient, + state_root: header.state_root, + receipts_root: header.receipts_root, + logs_bloom: header.logs_bloom, + prev_randao: header.prev_randao, + block_number: header.block_number, + gas_limit: header.gas_limit, + gas_used: header.gas_used, + timestamp: header.timestamp, + extra_data: header.extra_data, + base_fee_per_gas: header.base_fee_per_gas, + block_hash: header.block_hash, + transactions: self.transactions, + withdrawals, + blob_gas_used: header.blob_gas_used, + excess_blob_gas: header.excess_blob_gas, + })) + } else { + Err(format!( + "block {} is post capella but payload body doesn't have withdrawals", + header.block_hash + )) + } + } + ExecutionPayloadHeader::Gloas(header) => { + if let Some(withdrawals) = self.withdrawals { + Ok(ExecutionPayload::Gloas(ExecutionPayloadGloas { + parent_hash: header.parent_hash, + fee_recipient: header.fee_recipient, + state_root: header.state_root, + receipts_root: header.receipts_root, + logs_bloom: header.logs_bloom, + prev_randao: header.prev_randao, + block_number: header.block_number, + gas_limit: header.gas_limit, + gas_used: header.gas_used, + timestamp: header.timestamp, + extra_data: header.extra_data, + base_fee_per_gas: header.base_fee_per_gas, + block_hash: header.block_hash, + transactions: self.transactions, + withdrawals, + blob_gas_used: header.blob_gas_used, + excess_blob_gas: header.excess_blob_gas, + // V2 provides block_access_list from EL; use empty if not present + block_access_list: self + .block_access_list + .unwrap_or_else(VariableList::empty), + slot_number: header.slot_number, + })) + } else { + Err(format!( + "block {} is post capella but payload body doesn't have withdrawals", + header.block_hash + )) + } + } + } + } +} + +impl ExecutionPayloadBody { + pub fn to_payload( + self, + header: ExecutionPayloadHeader, + ) -> Result, String> { + match self { + ExecutionPayloadBody::V1(body) => body.to_payload(header), + ExecutionPayloadBody::V2(body) => body.to_payload(header), + } + } +} + #[derive(Clone, Copy, Debug)] pub struct EngineCapabilities { pub new_payload_v1: bool, @@ -620,7 +818,9 @@ 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_bodies_by_range_v2: bool, pub get_payload_v1: bool, pub get_payload_v2: bool, pub get_payload_v3: bool, @@ -665,9 +865,15 @@ 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); } + 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 92f4cee563..e7c84528e1 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -54,7 +54,9 @@ 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_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"; @@ -90,7 +92,9 @@ 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_PAYLOAD_BODIES_BY_RANGE_V2, ENGINE_GET_CLIENT_VERSION_V1, ENGINE_GET_BLOBS_V1, ENGINE_GET_BLOBS_V2, @@ -1364,6 +1368,58 @@ 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_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]); @@ -1387,8 +1443,12 @@ 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_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/engine_api/json_structures.rs b/beacon_node/execution_layer/src/engine_api/json_structures.rs index b1c009fcef..a5b7fda3b9 100644 --- a/beacon_node/execution_layer/src/engine_api/json_structures.rs +++ b/beacon_node/execution_layer/src/engine_api/json_structures.rs @@ -1020,12 +1020,24 @@ impl From for JsonForkchoiceUpdatedV1Response { } } +#[superstruct( + variants(V1, V2), + variant_attributes( + derive(Clone, Debug, Serialize, Deserialize), + serde(bound = "E: EthSpec", rename_all = "camelCase") + ), + cast_error(ty = "Error", expr = "Error::IncorrectStateVariant"), + partial_getter_error(ty = "Error", expr = "Error::IncorrectStateVariant") +)] #[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(bound = "E: EthSpec")] -pub struct JsonExecutionPayloadBodyV1 { +#[serde(bound = "E: EthSpec", rename_all = "camelCase")] +pub struct JsonExecutionPayloadBody { #[serde(with = "ssz_types::serde_utils::list_of_hex_var_list")] pub transactions: Transactions, pub withdrawals: Option>, + #[superstruct(only(V2))] + #[serde(with = "optional_hex_var_list")] + pub block_access_list: Option>, } impl TryFrom> for ExecutionPayloadBodyV1 { @@ -1050,6 +1062,30 @@ impl TryFrom> for JsonExecutionPayloadBody } } +impl TryFrom> for ExecutionPayloadBodyV2 { + type Error = ssz_types::Error; + + fn try_from(value: JsonExecutionPayloadBodyV2) -> Result { + Ok(Self { + transactions: value.transactions, + withdrawals: value.withdrawals.map(withdrawals_from_json).transpose()?, + block_access_list: value.block_access_list, + }) + } +} + +impl TryFrom> for JsonExecutionPayloadBodyV2 { + type Error = ssz_types::Error; + + fn try_from(value: ExecutionPayloadBodyV2) -> Result { + Ok(Self { + transactions: value.transactions, + withdrawals: value.withdrawals.map(withdrawals_to_json).transpose()?, + block_access_list: value.block_access_list, + }) + } +} + #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TransitionConfigurationV1 { @@ -1090,6 +1126,50 @@ pub mod serde_logs_bloom { } } +/// Serializes an optional hex variable list field (e.g., blockAccessList in EIP-7928). +/// JSON `null` maps to `None`, hex string maps to `Some(VariableList)`. +pub mod optional_hex_var_list { + use super::*; + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize( + opt_bytes: &Option>, + serializer: S, + ) -> Result + where + S: Serializer, + N: Unsigned, + { + match opt_bytes { + Some(bytes) => { + let mut hex_string: String = "0x".to_string(); + hex_string.push_str(&hex::encode(&bytes[..])); + serializer.serialize_str(&hex_string) + } + None => serializer.serialize_none(), + } + } + + pub fn deserialize<'de, D, N>(deserializer: D) -> Result>, D::Error> + where + D: Deserializer<'de>, + N: Unsigned, + { + let opt: Option = Option::deserialize(deserializer)?; + match opt { + Some(hex_str) => { + let hex_str = hex_str.strip_prefix("0x").unwrap_or(&hex_str); + let bytes = hex::decode(hex_str) + .map_err(|e| serde::de::Error::custom(format!("invalid hex: {:?}", e)))?; + VariableList::new(bytes) + .map(Some) + .map_err(|e| serde::de::Error::custom(format!("invalid var list: {:?}", e))) + } + None => Ok(None), + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct JsonClientVersionV1 { diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index 37b514f480..f5c7231b20 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -158,6 +158,7 @@ pub enum Error { }, ZeroLengthTransaction, PayloadBodiesByRangeNotSupported, + PayloadBodiesByHashNotSupported, GetBlobsNotSupported, InvalidJWTSecret(String), InvalidForkForPayload, @@ -1787,32 +1788,87 @@ impl ExecutionLayer { pub async fn get_payload_bodies_by_hash( &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) + ) -> Result>>, Error> { + let capabilities = self.get_engine_capabilities(None).await?; + + if capabilities.get_payload_bodies_by_hash_v2 { + self.engine() + .request(|engine: &Engine| async move { + engine.api.get_payload_bodies_by_hash_v2(hashes).await + }) + .await + .map(|bodies| { + bodies + .into_iter() + .map(|opt| opt.map(ExecutionPayloadBody::V2)) + .collect() + }) + .map_err(Box::new) + .map_err(Error::EngineError) + } else if capabilities.get_payload_bodies_by_hash_v1 { + self.engine() + .request(|engine: &Engine| async move { + engine.api.get_payload_bodies_by_hash_v1::(hashes).await + }) + .await + .map(|bodies| { + bodies + .into_iter() + .map(|opt| opt.map(ExecutionPayloadBody::V1)) + .collect() + }) + .map_err(Box::new) + .map_err(Error::EngineError) + } else { + Err(Error::PayloadBodiesByHashNotSupported) + } } pub async fn get_payload_bodies_by_range( &self, start: u64, count: u64, - ) -> Result>>, Error> { + ) -> Result>>, Error> { 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 - }) - .await - .map_err(Box::new) - .map_err(Error::EngineError) + let capabilities = self.get_engine_capabilities(None).await?; + + if capabilities.get_payload_bodies_by_range_v2 { + self.engine() + .request(|engine: &Engine| async move { + engine + .api + .get_payload_bodies_by_range_v2(start, count) + .await + }) + .await + .map(|bodies| { + bodies + .into_iter() + .map(|opt| opt.map(ExecutionPayloadBody::V2)) + .collect() + }) + .map_err(Box::new) + .map_err(Error::EngineError) + } else if capabilities.get_payload_bodies_by_range_v1 { + self.engine() + .request(|engine: &Engine| async move { + engine + .api + .get_payload_bodies_by_range_v1::(start, count) + .await + }) + .await + .map(|bodies| { + bodies + .into_iter() + .map(|opt| opt.map(ExecutionPayloadBody::V1)) + .collect() + }) + .map_err(Box::new) + .map_err(Error::EngineError) + } else { + Err(Error::PayloadBodiesByRangeNotSupported) + } } /// Fetch a full payload from the execution node. @@ -1845,7 +1901,9 @@ 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_v2 + || 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 { diff --git a/beacon_node/execution_layer/src/test_utils/mod.rs b/beacon_node/execution_layer/src/test_utils/mod.rs index 98611e79a1..1167675dd3 100644 --- a/beacon_node/execution_layer/src/test_utils/mod.rs +++ b/beacon_node/execution_layer/src/test_utils/mod.rs @@ -51,7 +51,9 @@ 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_bodies_by_range_v2: true, get_payload_v1: true, get_payload_v2: true, get_payload_v3: true,