Engine api changes

This commit is contained in:
Eitan Seri-Levi
2026-04-30 11:51:23 +02:00
parent 4d97dd774d
commit 9a5feb67c6
12 changed files with 232 additions and 18 deletions

View File

@@ -1,11 +1,12 @@
use crate::engines::ForkchoiceState;
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_BLOBS_V3,
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_FORKCHOICE_UPDATED_V4, ENGINE_FORKCHOICE_UPDATED_V5, ENGINE_GET_BLOBS_V1,
ENGINE_GET_BLOBS_V2, ENGINE_GET_BLOBS_V3, 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_IS_INCLUSION_LIST_SATISFIED_V1, ENGINE_NEW_PAYLOAD_V1, ENGINE_NEW_PAYLOAD_V2,
ENGINE_NEW_PAYLOAD_V3, ENGINE_NEW_PAYLOAD_V4, ENGINE_NEW_PAYLOAD_V5,
};
use eth2::types::{
@@ -159,7 +160,7 @@ impl ExecutionBlock {
}
#[superstruct(
variants(V1, V2, V3, V4),
variants(V1, V2, V3, V4, V5),
variant_attributes(derive(Clone, Debug, Eq, Hash, PartialEq),),
cast_error(ty = "Error", expr = "Error::IncorrectStateVariant"),
partial_getter_error(ty = "Error", expr = "Error::IncorrectStateVariant")
@@ -172,12 +173,14 @@ pub struct PayloadAttributes {
pub prev_randao: Hash256,
#[superstruct(getter(copy))]
pub suggested_fee_recipient: Address,
#[superstruct(only(V2, V3, V4))]
#[superstruct(only(V2, V3, V4, V5))]
pub withdrawals: Vec<Withdrawal>,
#[superstruct(only(V3, V4), partial_getter(copy))]
#[superstruct(only(V3, V4, V5), partial_getter(copy))]
pub parent_beacon_block_root: Hash256,
#[superstruct(only(V4), partial_getter(copy))]
#[superstruct(only(V4, V5), partial_getter(copy))]
pub slot_number: u64,
#[superstruct(only(V5))]
pub inclusion_list_transactions: Vec<Vec<u8>>,
}
impl PayloadAttributes {
@@ -188,9 +191,29 @@ impl PayloadAttributes {
withdrawals: Option<Vec<Withdrawal>>,
parent_beacon_block_root: Option<Hash256>,
slot_number: Option<u64>,
inclusion_list_transactions: Option<Vec<Vec<u8>>>,
) -> Self {
match (withdrawals, parent_beacon_block_root, slot_number) {
(Some(withdrawals), Some(parent_beacon_block_root), Some(slot_number)) => {
match (
withdrawals,
parent_beacon_block_root,
slot_number,
inclusion_list_transactions,
) {
(
Some(withdrawals),
Some(parent_beacon_block_root),
Some(slot_number),
Some(inclusion_list_transactions),
) => PayloadAttributes::V5(PayloadAttributesV5 {
timestamp,
prev_randao,
suggested_fee_recipient,
withdrawals,
parent_beacon_block_root,
slot_number,
inclusion_list_transactions,
}),
(Some(withdrawals), Some(parent_beacon_block_root), Some(slot_number), None) => {
PayloadAttributes::V4(PayloadAttributesV4 {
timestamp,
prev_randao,
@@ -200,7 +223,7 @@ impl PayloadAttributes {
slot_number,
})
}
(Some(withdrawals), Some(parent_beacon_block_root), None) => {
(Some(withdrawals), Some(parent_beacon_block_root), None, _) => {
PayloadAttributes::V3(PayloadAttributesV3 {
timestamp,
prev_randao,
@@ -209,13 +232,13 @@ impl PayloadAttributes {
parent_beacon_block_root,
})
}
(Some(withdrawals), None, _) => PayloadAttributes::V2(PayloadAttributesV2 {
(Some(withdrawals), None, _, _) => PayloadAttributes::V2(PayloadAttributesV2 {
timestamp,
prev_randao,
suggested_fee_recipient,
withdrawals,
}),
(None, _, _) => PayloadAttributes::V1(PayloadAttributesV1 {
(None, _, _, _) => PayloadAttributes::V1(PayloadAttributesV1 {
timestamp,
prev_randao,
suggested_fee_recipient,
@@ -275,6 +298,22 @@ impl From<PayloadAttributes> for SsePayloadAttributes {
withdrawals,
parent_beacon_block_root,
}),
// V5 maps to V3 for SSE (slot_number and inclusion_list_transactions are not part of the SSE spec)
PayloadAttributes::V5(PayloadAttributesV5 {
timestamp,
prev_randao,
suggested_fee_recipient,
withdrawals,
parent_beacon_block_root,
slot_number: _,
inclusion_list_transactions: _,
}) => Self::V3(SsePayloadAttributesV3 {
timestamp,
prev_randao,
suggested_fee_recipient,
withdrawals,
parent_beacon_block_root,
}),
}
}
}
@@ -637,6 +676,8 @@ pub struct EngineCapabilities {
pub get_blobs_v2: bool,
pub get_inclusion_list_v1: bool,
pub get_blobs_v3: bool,
pub forkchoice_updated_v5: bool,
pub is_inclusion_list_satisfied_v1: bool,
}
impl EngineCapabilities {
@@ -708,6 +749,12 @@ impl EngineCapabilities {
if self.get_blobs_v3 {
response.push(ENGINE_GET_BLOBS_V3);
}
if self.forkchoice_updated_v5 {
response.push(ENGINE_FORKCHOICE_UPDATED_V5);
}
if self.is_inclusion_list_satisfied_v1 {
response.push(ENGINE_IS_INCLUSION_LIST_SATISFIED_V1);
}
response
}

View File

@@ -50,6 +50,7 @@ pub const ENGINE_FORKCHOICE_UPDATED_V1: &str = "engine_forkchoiceUpdatedV1";
pub const ENGINE_FORKCHOICE_UPDATED_V2: &str = "engine_forkchoiceUpdatedV2";
pub const ENGINE_FORKCHOICE_UPDATED_V3: &str = "engine_forkchoiceUpdatedV3";
pub const ENGINE_FORKCHOICE_UPDATED_V4: &str = "engine_forkchoiceUpdatedV4";
pub const ENGINE_FORKCHOICE_UPDATED_V5: &str = "engine_forkchoiceUpdatedV5";
pub const ENGINE_FORKCHOICE_UPDATED_TIMEOUT: Duration = Duration::from_secs(8);
pub const ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1: &str = "engine_getPayloadBodiesByHashV1";
@@ -70,6 +71,9 @@ pub const ENGINE_GET_BLOBS_TIMEOUT: Duration = Duration::from_secs(1);
pub const ENGINE_GET_INCLUSION_LIST_V1: &str = "engine_getInclusionListV1";
pub const ENGINE_GET_INCLUSION_LIST_TIMEOUT: Duration = Duration::from_secs(1);
pub const ENGINE_IS_INCLUSION_LIST_SATISFIED_V1: &str = "engine_isInclusionListSatisfiedV1";
pub const ENGINE_IS_INCLUSION_LIST_SATISFIED_TIMEOUT: Duration = Duration::from_secs(1);
/// This error is returned during a `chainId` call by Geth.
pub const EIP155_ERROR_STR: &str = "chain not synced beyond EIP-155 replay-protection fork block";
/// This code is returned by all clients when a method is not supported
@@ -92,12 +96,14 @@ pub static LIGHTHOUSE_CAPABILITIES: &[&str] = &[
ENGINE_FORKCHOICE_UPDATED_V2,
ENGINE_FORKCHOICE_UPDATED_V3,
ENGINE_FORKCHOICE_UPDATED_V4,
ENGINE_FORKCHOICE_UPDATED_V5,
ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1,
ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1,
ENGINE_GET_CLIENT_VERSION_V1,
ENGINE_GET_BLOBS_V1,
ENGINE_GET_BLOBS_V2,
ENGINE_GET_INCLUSION_LIST_V1,
ENGINE_IS_INCLUSION_LIST_SATISFIED_V1,
];
/// We opt to initialize the JsonClientVersionV1 rather than the ClientVersionV1
@@ -1314,6 +1320,47 @@ impl HttpJsonRpc {
Ok(response.into())
}
pub async fn forkchoice_updated_v5(
&self,
forkchoice_state: ForkchoiceState,
payload_attributes: Option<PayloadAttributes>,
) -> Result<ForkchoiceUpdatedResponse, Error> {
let params = json!([
JsonForkchoiceStateV1::from(forkchoice_state),
payload_attributes.map(JsonPayloadAttributes::from)
]);
let response: JsonForkchoiceUpdatedV1Response = self
.rpc_request(
ENGINE_FORKCHOICE_UPDATED_V5,
params,
ENGINE_FORKCHOICE_UPDATED_TIMEOUT * self.execution_timeout_multiplier,
)
.await?;
Ok(response.into())
}
pub async fn is_inclusion_list_satisfied(
&self,
execution_payload_hash: ExecutionBlockHash,
inclusion_list_transactions: Vec<Vec<u8>>,
) -> Result<bool, Error> {
let hex_transactions: Vec<String> = inclusion_list_transactions
.into_iter()
.map(|tx| format!("0x{}", hex::encode(tx)))
.collect();
let params = json!([execution_payload_hash, hex_transactions]);
self.rpc_request(
ENGINE_IS_INCLUSION_LIST_SATISFIED_V1,
params,
ENGINE_IS_INCLUSION_LIST_SATISFIED_TIMEOUT * self.execution_timeout_multiplier,
)
.await
}
pub async fn get_payload_bodies_by_hash_v1<E: EthSpec>(
&self,
block_hashes: Vec<ExecutionBlockHash>,
@@ -1402,6 +1449,9 @@ impl HttpJsonRpc {
get_blobs_v2: capabilities.contains(ENGINE_GET_BLOBS_V2),
get_inclusion_list_v1: capabilities.contains(ENGINE_GET_INCLUSION_LIST_V1),
get_blobs_v3: capabilities.contains(ENGINE_GET_BLOBS_V3),
forkchoice_updated_v5: capabilities.contains(ENGINE_FORKCHOICE_UPDATED_V5),
is_inclusion_list_satisfied_v1: capabilities
.contains(ENGINE_IS_INCLUSION_LIST_SATISFIED_V1),
})
}
@@ -1675,6 +1725,16 @@ impl HttpJsonRpc {
))
}
}
PayloadAttributes::V5(_) => {
if engine_capabilities.forkchoice_updated_v5 {
self.forkchoice_updated_v5(forkchoice_state, maybe_payload_attributes)
.await
} else {
Err(Error::RequiredMethodUnsupported(
"engine_forkchoiceUpdatedV5",
))
}
}
}
} else if engine_capabilities.forkchoice_updated_v3 {
self.forkchoice_updated_v3(forkchoice_state, maybe_payload_attributes)

View File

@@ -831,7 +831,7 @@ impl<'a> From<&'a JsonWithdrawal> for EncodableJsonWithdrawal<'a> {
}
#[superstruct(
variants(V1, V2, V3, V4),
variants(V1, V2, V3, V4, V5),
variant_attributes(
derive(Debug, Clone, PartialEq, Serialize, Deserialize),
serde(rename_all = "camelCase")
@@ -847,13 +847,15 @@ pub struct JsonPayloadAttributes {
pub prev_randao: Hash256,
#[serde(with = "serde_utils::address_hex")]
pub suggested_fee_recipient: Address,
#[superstruct(only(V2, V3, V4))]
#[superstruct(only(V2, V3, V4, V5))]
pub withdrawals: Vec<JsonWithdrawal>,
#[superstruct(only(V3, V4))]
#[superstruct(only(V3, V4, V5))]
pub parent_beacon_block_root: Hash256,
#[superstruct(only(V4))]
#[superstruct(only(V4, V5))]
#[serde(with = "serde_utils::u64_hex_be")]
pub slot_number: u64,
#[superstruct(only(V5))]
pub inclusion_list_transactions: Vec<String>,
}
impl From<PayloadAttributes> for JsonPayloadAttributes {
@@ -885,6 +887,19 @@ impl From<PayloadAttributes> for JsonPayloadAttributes {
parent_beacon_block_root: pa.parent_beacon_block_root,
slot_number: pa.slot_number,
}),
PayloadAttributes::V5(pa) => Self::V5(JsonPayloadAttributesV5 {
timestamp: pa.timestamp,
prev_randao: pa.prev_randao,
suggested_fee_recipient: pa.suggested_fee_recipient,
withdrawals: pa.withdrawals.into_iter().map(Into::into).collect(),
parent_beacon_block_root: pa.parent_beacon_block_root,
slot_number: pa.slot_number,
inclusion_list_transactions: pa
.inclusion_list_transactions
.into_iter()
.map(|tx| format!("0x{}", hex::encode(tx)))
.collect(),
}),
}
}
}
@@ -918,6 +933,21 @@ impl From<JsonPayloadAttributes> for PayloadAttributes {
parent_beacon_block_root: jpa.parent_beacon_block_root,
slot_number: jpa.slot_number,
}),
JsonPayloadAttributes::V5(jpa) => Self::V5(PayloadAttributesV5 {
timestamp: jpa.timestamp,
prev_randao: jpa.prev_randao,
suggested_fee_recipient: jpa.suggested_fee_recipient,
withdrawals: jpa.withdrawals.into_iter().map(Into::into).collect(),
parent_beacon_block_root: jpa.parent_beacon_block_root,
slot_number: jpa.slot_number,
inclusion_list_transactions: jpa
.inclusion_list_transactions
.into_iter()
.map(|s| {
hex::decode(s.strip_prefix("0x").unwrap_or(&s)).unwrap_or_default()
})
.collect(),
}),
}
}
}

View File

@@ -806,6 +806,30 @@ impl<E: EthSpec> ExecutionBlockGenerator<E> {
}),
_ => unreachable!(),
},
PayloadAttributes::V5(pa) => match self.get_fork_at_timestamp(pa.timestamp) {
ForkName::Heze => ExecutionPayload::Heze(ExecutionPayloadHeze {
parent_hash: head_block_hash,
fee_recipient: pa.suggested_fee_recipient,
receipts_root: Hash256::repeat_byte(42),
state_root: Hash256::repeat_byte(43),
logs_bloom: vec![0; 256].try_into().unwrap(),
prev_randao: pa.prev_randao,
block_number: parent.block_number() + 1,
gas_limit: DEFAULT_GAS_LIMIT,
gas_used: GAS_USED,
timestamp: pa.timestamp,
extra_data: "block gen was here".as_bytes().to_vec().try_into().unwrap(),
base_fee_per_gas: Uint256::from(1u64),
block_hash: ExecutionBlockHash::zero(),
transactions: vec![].try_into().unwrap(),
withdrawals: pa.withdrawals.clone().try_into().unwrap(),
blob_gas_used: 0,
excess_blob_gas: 0,
block_access_list: VariableList::empty(),
slot_number: pa.slot_number.into(),
}),
_ => unreachable!(),
},
};
// Store execution requests for this payload if configured.

View File

@@ -948,6 +948,7 @@ impl<E: EthSpec> MockBuilder<E> {
expected_withdrawals,
None,
None,
None,
),
ForkName::Deneb | ForkName::Electra | ForkName::Fulu => PayloadAttributes::new(
timestamp,
@@ -956,6 +957,7 @@ impl<E: EthSpec> MockBuilder<E> {
expected_withdrawals,
Some(head_block_root),
None,
None,
),
ForkName::Gloas | ForkName::Heze => PayloadAttributes::new(
timestamp,
@@ -964,6 +966,7 @@ impl<E: EthSpec> MockBuilder<E> {
expected_withdrawals,
Some(head_block_root),
Some(slot.as_u64()),
None,
),
ForkName::Base | ForkName::Altair => {
return Err("invalid fork".to_string());

View File

@@ -108,6 +108,7 @@ impl<E: EthSpec> MockExecutionLayer<E> {
None,
None,
None,
None,
);
// Insert a proposer to ensure the fork choice updated command works.
@@ -149,6 +150,7 @@ impl<E: EthSpec> MockExecutionLayer<E> {
None,
None,
None,
None,
);
let payload_parameters = PayloadParameters {
@@ -202,6 +204,7 @@ impl<E: EthSpec> MockExecutionLayer<E> {
None,
None,
None,
None,
);
let payload_parameters = PayloadParameters {

View File

@@ -61,6 +61,8 @@ pub const DEFAULT_ENGINE_CAPABILITIES: EngineCapabilities = EngineCapabilities {
get_blobs_v2: true,
get_inclusion_list_v1: true,
get_blobs_v3: true,
forkchoice_updated_v5: true,
is_inclusion_list_satisfied_v1: true,
};
pub static DEFAULT_CLIENT_VERSION: LazyLock<JsonClientVersionV1> =