diff --git a/beacon_node/execution_layer/src/engine_api.rs b/beacon_node/execution_layer/src/engine_api.rs index 52a58c88fec..f306d3e54cd 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_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_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, @@ -20,14 +21,14 @@ 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, ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionPayloadGloas, ExecutionPayloadHeze, - ExecutionRequests, KzgProofs, + ExecutionRequests, KzgProofs, ProgressiveTransactions, ProgressiveWithdrawals, }; use types::{GRAFFITI_BYTES_LEN, Graffiti}; @@ -449,6 +450,15 @@ pub struct ExecutionPayloadBodyV1 { 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, +} + impl ExecutionPayloadBodyV1 { pub fn to_payload( self, @@ -605,7 +615,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, @@ -651,9 +663,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 b6e755471e0..e83a9c3d8f9 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -55,7 +55,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"; @@ -77,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, @@ -94,7 +98,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_V2, ENGINE_GET_BLOBS_V3, @@ -1204,6 +1210,26 @@ 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?; + + Ok(response + .into_iter() + .map(|body| body.map(Into::into)) + .collect()) + } + pub async fn get_payload_bodies_by_range_v1( &self, start: u64, @@ -1255,8 +1281,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 b6f5f820034..20bbe8a6434 100644 --- a/beacon_node/execution_layer/src/engine_api/json_structures.rs +++ b/beacon_node/execution_layer/src/engine_api/json_structures.rs @@ -1207,6 +1207,12 @@ impl From for JsonForkchoiceUpdatedV1Response { } } +#[derive(Clone, Debug, Serialize, Deserialize)] +#[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")] pub struct JsonExecutionPayloadBodyV1 { @@ -1215,6 +1221,40 @@ pub struct JsonExecutionPayloadBodyV1 { 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)] + 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; @@ -1651,4 +1691,47 @@ 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: 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])) + ); + assert_eq!(serde_json::to_value(&body).unwrap(), with_bal); + + // 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.clone()).unwrap(); + let internal: ExecutionPayloadBodyV2 = body.clone().into(); + assert_eq!(internal.block_access_list, None); + assert_eq!(serde_json::to_value(&body).unwrap(), null_bal); + + // 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 29236ca7687..771cdfa86a9 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -154,6 +154,7 @@ pub enum Error { transactions_root: Hash256, }, ZeroLengthTransaction, + PayloadBodiesByHashV2NotSupported, PayloadBodiesByRangeNotSupported, GetBlobsNotSupported, GetInclusionListNotSupported, @@ -1674,6 +1675,25 @@ impl ExecutionLayer { .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?; + if !capabilities.get_payload_bodies_by_hash_v2 { + return Err(Error::PayloadBodiesByHashV2NotSupported); + } + + self.engine() + .request(|engine: &Engine| async move { + engine.api.get_payload_bodies_by_hash_v2(hashes).await + }) + .await + .map_err(Box::new) + .map_err(Error::EngineError) + } + pub async fn get_payload_bodies_by_range( &self, start: u64, @@ -2175,7 +2195,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; @@ -2193,6 +2213,55 @@ mod test { .await; } + #[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()); + let block_hash = ExecutionBlockHash::repeat_byte(0x42); + let block_number = 42; + 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 { + 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]); + } + #[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 c1e72a6f3ed..8dd08a9223d 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; @@ -768,6 +769,44 @@ pub async fn handle_rpc( Ok(serde_json::to_value(response).unwrap()) } + 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)] diff --git a/beacon_node/execution_layer/src/test_utils/mod.rs b/beacon_node/execution_layer/src/test_utils/mod.rs index 82b55551367..9294990bfcb 100644 --- a/beacon_node/execution_layer/src/test_utils/mod.rs +++ b/beacon_node/execution_layer/src/test_utils/mod.rs @@ -49,7 +49,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,