Skip to content
28 changes: 23 additions & 5 deletions beacon_node/execution_layer/src/engine_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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};

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

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

impl<E: EthSpec> ExecutionPayloadBodyV1<E> {
pub fn to_payload(
self,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
Expand Down
30 changes: 30 additions & 0 deletions beacon_node/execution_layer/src/engine_api/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -1204,6 +1210,26 @@ 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_payload_bodies_by_range_v1<E: EthSpec>(
&self,
start: u64,
Expand Down Expand Up @@ -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),
Expand Down
83 changes: 83 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 @@ -1207,6 +1207,12 @@ impl From<ForkchoiceUpdatedResponse> 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<E: EthSpec> {
Expand All @@ -1215,6 +1221,40 @@ 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: Option<ProgressiveVariableList<JsonWithdrawal>>,
#[serde(default)]
pub block_access_list: Option<JsonBlockAccessList>,
}

impl From<JsonExecutionPayloadBodyV2> 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<ExecutionPayloadBodyV2> 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<E: EthSpec> TryFrom<JsonExecutionPayloadBodyV1<E>> for ExecutionPayloadBodyV1<E> {
type Error = ssz_types::Error;

Expand Down Expand Up @@ -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,
})
);
}
}
71 changes: 70 additions & 1 deletion beacon_node/execution_layer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ pub enum Error {
transactions_root: Hash256,
},
ZeroLengthTransaction,
PayloadBodiesByHashV2NotSupported,
PayloadBodiesByRangeNotSupported,
GetBlobsNotSupported,
GetInclusionListNotSupported,
Expand Down Expand Up @@ -1674,6 +1675,25 @@ impl<E: EthSpec> ExecutionLayer<E> {
.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<ExecutionBlockHash>,
) -> Result<Vec<Option<ExecutionPayloadBodyV2>>, 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,
Expand Down Expand Up @@ -2175,7 +2195,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 +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();
Expand Down
39 changes: 39 additions & 0 deletions beacon_node/execution_layer/src/test_utils/handle_rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -768,6 +769,44 @@ pub async fn handle_rpc<E: EthSpec>(

Ok(serde_json::to_value(response).unwrap())
}
ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V2 => {
let block_hashes = get_param::<Vec<ExecutionBlockHash>>(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)]
Expand Down
Loading
Loading