Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 69 additions & 6 deletions beacon_node/execution_layer/src/engine_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -25,9 +25,9 @@ pub use types::{
Withdrawal, Withdrawals,
};
use types::{
ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadDeneb,
BlockAccessList, ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadDeneb,
ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionPayloadGloas, ExecutionPayloadHeze,
ExecutionRequests, KzgProofs,
ExecutionRequests, KzgProofs, ProgressiveTransactions, ProgressiveWithdrawals, Slot,
};
use types::{GRAFFITI_BYTES_LEN, Graffiti};

Expand Down Expand Up @@ -449,6 +449,65 @@ pub struct ExecutionPayloadBodyV1<E: EthSpec> {
pub withdrawals: Option<Withdrawals<E>>,
}

/// The execution payload body returned by `engine_getPayloadBodiesByHashV2`.
#[derive(Clone, Debug)]
pub struct ExecutionPayloadBodyV2 {
pub transactions: ProgressiveTransactions,
pub withdrawals: ProgressiveWithdrawals,
pub block_access_list: BlockAccessList,
}

/// Header fields obtained from `eth_getBlockByHash` which, together with an
/// `ExecutionPayloadBodyV2`, comprise a Gloas execution payload.
#[derive(Clone, Debug)]
pub struct ExecutionBlockHeaderGloas<E: EthSpec> {
Comment thread
hopinheimer marked this conversation as resolved.
Outdated
pub parent_hash: ExecutionBlockHash,
pub fee_recipient: Address,
pub state_root: Hash256,
pub receipts_root: Hash256,
pub logs_bloom: ssz_types::FixedVector<u8, E::BytesPerLogsBloom>,
pub prev_randao: Hash256,
pub block_number: u64,
pub gas_limit: u64,
pub gas_used: u64,
pub timestamp: u64,
pub extra_data: ssz_types::VariableList<u8, E::MaxExtraDataBytes>,
pub base_fee_per_gas: Uint256,
pub block_hash: ExecutionBlockHash,
pub blob_gas_used: u64,
pub excess_blob_gas: u64,
}

impl ExecutionPayloadBodyV2 {
pub fn to_payload<E: EthSpec>(
self,
header: ExecutionBlockHeaderGloas<E>,
slot: Slot,
) -> ExecutionPayloadGloas<E> {
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: self.withdrawals,
blob_gas_used: header.blob_gas_used,
excess_blob_gas: header.excess_blob_gas,
block_access_list: self.block_access_list,
slot_number: slot,
}
}
}

impl<E: EthSpec> ExecutionPayloadBodyV1<E> {
pub fn to_payload(
self,
Expand Down Expand Up @@ -605,6 +664,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,
Expand Down Expand Up @@ -651,6 +711,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);
}
Expand Down
52 changes: 52 additions & 0 deletions beacon_node/execution_layer/src/engine_api/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ pub const RETURN_FULL_TRANSACTION_OBJECTS: bool = false;

pub const ETH_GET_BLOCK_BY_NUMBER: &str = "eth_getBlockByNumber";
pub const ETH_GET_BLOCK_BY_NUMBER_TIMEOUT: Duration = Duration::from_secs(1);
pub const ETH_GET_BLOCK_BY_HASH: &str = "eth_getBlockByHash";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need eth_getBlockByHash? We just deleted it in:

It was a fallback for old ELs that didn't have the payload bodies method, we should not need it for Gloas (unless I'm missing something).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at this PR, it looks like you're using this data to reconstruct the full EL block. I think we should be able to get by without doing this, by just fetching the payload envelope body, and reconstructing the payload envelope (but never the full EL block). Avoiding eth_getBlockByHash saves us from extra EL-queries, code complexity, and from using non-engine APIs (which some ELs won't serve to us on the engine endpoint by default)

@hopinheimer hopinheimer Sep 7, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the current implementation we do. the engine_getPayloadBodiesByHashV2 responds with array of ExecutionPayloadBodyV2 which has 3 fields and is not sufficient to reconstruct the SignedExecutionPayloadEnvelope. There are two options to do so

  • use eth_getBlockByHash and reconstruct using the ExecutionBlockHeader.
  • save the header as part of the SignedExecutionPayloadEnvelopeSummary but this would add approximately 500bytes to the summary.

so it's either add weight to the summary or extra call to the EL. it does sound reasonable to remove the extra call to the EL.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to add weight to the summary. Doing 2 calls per payload instead of 1 would be a lot of extra roundtrips

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed here e01ebed

pub const ETH_GET_BLOCK_BY_HASH_TIMEOUT: Duration = Duration::from_secs(10);

pub const ETH_SYNCING: &str = "eth_syncing";
pub const ETH_SYNCING_TIMEOUT: Duration = Duration::from_secs(1);
Expand All @@ -55,6 +57,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);

Expand Down Expand Up @@ -94,6 +97,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,
Expand Down Expand Up @@ -1204,6 +1208,52 @@ impl HttpJsonRpc {
.collect::<Result<Vec<_>, _>>()
}

pub async fn get_payload_bodies_by_hash_v2(
&self,
block_hashes: Vec<ExecutionBlockHash>,
) -> Result<Vec<Option<ExecutionPayloadBodyV2>>, Error> {
let params = json!([block_hashes]);

let response: Vec<Option<JsonExecutionPayloadBodyV2>> = 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_execution_block_header_gloas<E: EthSpec>(
&self,
block_hash: ExecutionBlockHash,
) -> Result<Option<ExecutionBlockHeaderGloas<E>>, Error> {
let params = json!([block_hash, RETURN_FULL_TRANSACTION_OBJECTS]);
let response: Option<JsonExecutionBlockHeaderGloas<E>> = self
.rpc_request(
ETH_GET_BLOCK_BY_HASH,
params,
ETH_GET_BLOCK_BY_HASH_TIMEOUT * self.execution_timeout_multiplier,
)
.await?;

response
.map(|header| {
if header.block_hash != block_hash {
return Err(Error::BadResponse(format!(
"eth_getBlockByHash returned block {} for requested block {}",
header.block_hash, block_hash
)));
}
Ok(header.into())
})
.transpose()
}

pub async fn get_payload_bodies_by_range_v1<E: EthSpec>(
&self,
start: u64,
Expand Down Expand Up @@ -1255,6 +1305,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),
Expand Down
94 changes: 94 additions & 0 deletions beacon_node/execution_layer/src/engine_api/json_structures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1215,6 +1215,100 @@ pub struct JsonExecutionPayloadBodyV1<E: EthSpec> {
pub withdrawals: Option<VariableList<JsonWithdrawal, E::MaxWithdrawalsPerPayload>>,
}

#[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: ProgressiveVariableList<JsonWithdrawal>,
#[serde(with = "ssz_types::serde_utils::hex_prog_var_list")]
pub block_access_list: BlockAccessList,
}

impl From<JsonExecutionPayloadBodyV2> for ExecutionPayloadBodyV2 {
fn from(value: JsonExecutionPayloadBodyV2) -> Self {
Self {
transactions: value.transactions,
withdrawals: value.withdrawals.into_iter().map(Into::into).collect(),
block_access_list: value.block_access_list,
}
}
}

impl From<ExecutionPayloadBodyV2> for JsonExecutionPayloadBodyV2 {
fn from(value: ExecutionPayloadBodyV2) -> Self {
Self {
transactions: value.transactions,
withdrawals: value.withdrawals.into_iter().map(Into::into).collect(),
block_access_list: value.block_access_list,
}
}
}

/// The execution header fields returned by `eth_getBlockByHash` that are required to
/// reconstruct a Gloas execution payload. Unknown Ethereum block fields are ignored.
#[derive(Clone, Debug, Deserialize)]
#[serde(bound = "E: EthSpec", rename_all = "camelCase")]
pub struct JsonExecutionBlockHeaderGloas<E: EthSpec> {
pub parent_hash: ExecutionBlockHash,
#[serde(
rename = "miner",
alias = "feeRecipient",
with = "serde_utils::address_hex"
)]
pub fee_recipient: Address,
pub state_root: Hash256,
pub receipts_root: Hash256,
#[serde(with = "serde_logs_bloom")]
pub logs_bloom: FixedVector<u8, E::BytesPerLogsBloom>,
#[serde(rename = "mixHash", alias = "prevRandao")]
pub prev_randao: Hash256,
#[serde(
rename = "number",
alias = "blockNumber",
with = "serde_utils::u64_hex_be"
)]
pub block_number: u64,
#[serde(with = "serde_utils::u64_hex_be")]
pub gas_limit: u64,
#[serde(with = "serde_utils::u64_hex_be")]
pub gas_used: u64,
#[serde(with = "serde_utils::u64_hex_be")]
pub timestamp: u64,
#[serde(with = "ssz_types::serde_utils::hex_var_list")]
pub extra_data: VariableList<u8, E::MaxExtraDataBytes>,
#[serde(with = "serde_utils::u256_hex_be")]
pub base_fee_per_gas: Uint256,
#[serde(rename = "hash", alias = "blockHash")]
pub block_hash: ExecutionBlockHash,
#[serde(with = "serde_utils::u64_hex_be")]
pub blob_gas_used: u64,
#[serde(with = "serde_utils::u64_hex_be")]
pub excess_blob_gas: u64,
}

impl<E: EthSpec> From<JsonExecutionBlockHeaderGloas<E>> for ExecutionBlockHeaderGloas<E> {
fn from(value: JsonExecutionBlockHeaderGloas<E>) -> Self {
Self {
parent_hash: value.parent_hash,
fee_recipient: value.fee_recipient,
state_root: value.state_root,
receipts_root: value.receipts_root,
logs_bloom: value.logs_bloom,
prev_randao: value.prev_randao,
block_number: value.block_number,
gas_limit: value.gas_limit,
gas_used: value.gas_used,
timestamp: value.timestamp,
extra_data: value.extra_data,
base_fee_per_gas: value.base_fee_per_gas,
block_hash: value.block_hash,
blob_gas_used: value.blob_gas_used,
excess_blob_gas: value.excess_blob_gas,
}
}
}

impl<E: EthSpec> TryFrom<JsonExecutionPayloadBodyV1<E>> for ExecutionPayloadBodyV1<E> {
type Error = ssz_types::Error;

Expand Down
77 changes: 76 additions & 1 deletion beacon_node/execution_layer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1674,6 +1674,57 @@ impl<E: EthSpec> ExecutionLayer<E> {
.map_err(Error::EngineError)
}

/// Fetch Gloas execution payloads by hash. The Engine API supplies the payload bodies,
/// while `eth_getBlockByHash` supplies the execution header fields. The slot is consensus
/// data and must be supplied by the caller.
pub async fn get_payloads_by_hash_gloas(
&self,
requests: Vec<(ExecutionBlockHash, Slot)>,
) -> Result<Vec<Option<ExecutionPayloadGloas<E>>>, Error> {
self.engine()
.request(|engine: &Engine| async move {
let block_hashes = requests
.iter()
.map(|(block_hash, _)| *block_hash)
.collect::<Vec<_>>();
let bodies = engine
.api
.get_payload_bodies_by_hash_v2(block_hashes)
.await?;

if bodies.len() != requests.len() {
return Err(ApiError::BadResponse(format!(
"engine_getPayloadBodiesByHashV2 returned {} bodies for {} requests",
bodies.len(),
requests.len()
)));
}

let mut payloads = Vec::with_capacity(requests.len());
for ((block_hash, slot), body) in requests.into_iter().zip(bodies) {
let Some(body) = body else {
payloads.push(None);
continue;
};
let Some(header) = engine
.api
.get_execution_block_header_gloas(block_hash)
.await?
else {
payloads.push(None);
continue;
};

payloads.push(Some(body.to_payload(header, slot)));
}

Ok(payloads)
})
.await
.map_err(Box::new)
.map_err(Error::EngineError)
}

pub async fn get_payload_bodies_by_range(
&self,
start: u64,
Expand Down Expand Up @@ -2175,7 +2226,7 @@ fn noop<E: EthSpec>(
#[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;

Expand All @@ -2193,6 +2244,30 @@ mod test {
.await;
}

#[tokio::test]
async fn reconstruct_gloas_payload_by_hash() {
let runtime = TestRuntime::default();
let mock = MockExecutionLayer::default_params(runtime.task_executor.clone());
let block_hash = ExecutionBlockHash::repeat_byte(0x42);
let slot = Slot::new(12);
let payload = ExecutionPayloadGloas {
block_hash,
slot_number: slot,
..Default::default()
};
mock.server
.execution_block_generator()
.insert_block_without_checks(Block::PoS(payload.clone().into()));

let payloads = mock
.el
.get_payloads_by_hash_gloas(vec![(block_hash, slot)])
.await
.expect("payload reconstruction should succeed");

assert_eq!(payloads, vec![Some(payload)]);
}

#[tokio::test]
async fn test_expected_gas_limit() {
let spec = ChainSpec::mainnet();
Expand Down
Loading
Loading