use crate::engines::ForkchoiceState; use crate::http::{ ENGINE_FORKCHOICE_UPDATED_V1, ENGINE_FORKCHOICE_UPDATED_V2, ENGINE_FORKCHOICE_UPDATED_V3, 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_V3, ENGINE_GET_PAYLOAD_V4, ENGINE_GET_PAYLOAD_V5, ENGINE_NEW_PAYLOAD_V1, ENGINE_NEW_PAYLOAD_V2, ENGINE_NEW_PAYLOAD_V3, ENGINE_NEW_PAYLOAD_V4, }; use eth2::types::{ BlobsBundle, SsePayloadAttributes, SsePayloadAttributesV1, SsePayloadAttributesV2, SsePayloadAttributesV3, }; use http::deposit_methods::RpcError; pub use json_structures::{JsonWithdrawal, TransitionConfigurationV1}; use pretty_reqwest_error::PrettyReqwestError; use reqwest::StatusCode; use serde::{Deserialize, Serialize}; use strum::IntoStaticStr; use superstruct::superstruct; pub use types::{ Address, BeaconBlockRef, ConsolidationRequest, EthSpec, ExecutionBlockHash, ExecutionPayload, ExecutionPayloadHeader, ExecutionPayloadRef, FixedVector, ForkName, Hash256, Transactions, Uint256, VariableList, Withdrawal, Withdrawals, }; use types::{ ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadDeneb, ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionPayloadGloas, ExecutionRequests, KzgProofs, }; use types::{GRAFFITI_BYTES_LEN, Graffiti}; pub mod auth; pub mod http; pub mod json_structures; mod new_payload_request; pub use new_payload_request::{ NewPayloadRequest, NewPayloadRequestBellatrix, NewPayloadRequestCapella, NewPayloadRequestDeneb, NewPayloadRequestElectra, NewPayloadRequestFulu, NewPayloadRequestGloas, }; pub const LATEST_TAG: &str = "latest"; pub type PayloadId = [u8; 8]; #[derive(Debug)] pub enum Error { HttpClient(PrettyReqwestError), Auth(auth::Error), BadResponse(String), RequestFailed(String), InvalidExecutePayloadResponse(&'static str), JsonRpc(RpcError), Json(serde_json::Error), ServerMessage { code: i64, message: String }, Eip155Failure, IsSyncing, ExecutionBlockNotFound(ExecutionBlockHash), ExecutionHeadBlockNotFound, ParentHashEqualsBlockHash(ExecutionBlockHash), PayloadIdUnavailable, SszError(ssz_types::Error), DeserializeWithdrawals(ssz_types::Error), DeserializeDepositRequests(ssz_types::Error), DeserializeWithdrawalRequests(ssz_types::Error), BuilderApi(builder_client::Error), IncorrectStateVariant, RequiredMethodUnsupported(&'static str), UnsupportedForkVariant(String), InvalidClientVersion(String), TooManyConsolidationRequests(usize), } impl From for Error { fn from(e: reqwest::Error) -> Self { if matches!( e.status(), Some(StatusCode::UNAUTHORIZED) | Some(StatusCode::FORBIDDEN) ) { Error::Auth(auth::Error::InvalidToken) } else { Error::HttpClient(e.into()) } } } impl From for Error { fn from(e: serde_json::Error) -> Self { Error::Json(e) } } impl From for Error { fn from(e: auth::Error) -> Self { Error::Auth(e) } } impl From for Error { fn from(e: builder_client::Error) -> Self { Error::BuilderApi(e) } } impl From for Error { fn from(e: ssz_types::Error) -> Self { Error::SszError(e) } } #[derive(Clone, Copy, Debug, PartialEq, IntoStaticStr)] #[strum(serialize_all = "snake_case")] pub enum PayloadStatusV1Status { Valid, Invalid, Syncing, Accepted, InvalidBlockHash, } #[derive(Clone, Debug, PartialEq)] pub struct PayloadStatusV1 { pub status: PayloadStatusV1Status, pub latest_valid_hash: Option, pub validation_error: Option, } #[derive(Clone, Copy, Debug, PartialEq, Serialize)] #[serde(untagged)] pub enum BlockByNumberQuery<'a> { Tag(&'a str), } /// Representation of an exection block with enough detail to determine the terminal PoW block. /// /// See `get_pow_block_hash_at_total_difficulty`. #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ExecutionBlock { #[serde(rename = "hash")] pub block_hash: ExecutionBlockHash, #[serde(rename = "number", with = "serde_utils::u64_hex_be")] pub block_number: u64, pub parent_hash: ExecutionBlockHash, pub total_difficulty: Option, #[serde(with = "serde_utils::u64_hex_be")] pub timestamp: u64, } impl ExecutionBlock { pub fn terminal_total_difficulty_reached(&self, terminal_total_difficulty: Uint256) -> bool { self.total_difficulty .is_none_or(|td| td >= terminal_total_difficulty) } } #[superstruct( variants(V1, V2, V3), variant_attributes(derive(Clone, Debug, Eq, Hash, PartialEq),), cast_error(ty = "Error", expr = "Error::IncorrectStateVariant"), partial_getter_error(ty = "Error", expr = "Error::IncorrectStateVariant") )] #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct PayloadAttributes { #[superstruct(getter(copy))] pub timestamp: u64, #[superstruct(getter(copy))] pub prev_randao: Hash256, #[superstruct(getter(copy))] pub suggested_fee_recipient: Address, #[superstruct(only(V2, V3))] pub withdrawals: Vec, #[superstruct(only(V3), partial_getter(copy))] pub parent_beacon_block_root: Hash256, } impl PayloadAttributes { pub fn new( timestamp: u64, prev_randao: Hash256, suggested_fee_recipient: Address, withdrawals: Option>, parent_beacon_block_root: Option, ) -> Self { match withdrawals { Some(withdrawals) => match parent_beacon_block_root { Some(parent_beacon_block_root) => PayloadAttributes::V3(PayloadAttributesV3 { timestamp, prev_randao, suggested_fee_recipient, withdrawals, parent_beacon_block_root, }), None => PayloadAttributes::V2(PayloadAttributesV2 { timestamp, prev_randao, suggested_fee_recipient, withdrawals, }), }, None => PayloadAttributes::V1(PayloadAttributesV1 { timestamp, prev_randao, suggested_fee_recipient, }), } } } impl From for SsePayloadAttributes { fn from(pa: PayloadAttributes) -> Self { match pa { PayloadAttributes::V1(PayloadAttributesV1 { timestamp, prev_randao, suggested_fee_recipient, }) => Self::V1(SsePayloadAttributesV1 { timestamp, prev_randao, suggested_fee_recipient, }), PayloadAttributes::V2(PayloadAttributesV2 { timestamp, prev_randao, suggested_fee_recipient, withdrawals, }) => Self::V2(SsePayloadAttributesV2 { timestamp, prev_randao, suggested_fee_recipient, withdrawals, }), PayloadAttributes::V3(PayloadAttributesV3 { timestamp, prev_randao, suggested_fee_recipient, withdrawals, parent_beacon_block_root, }) => Self::V3(SsePayloadAttributesV3 { timestamp, prev_randao, suggested_fee_recipient, withdrawals, parent_beacon_block_root, }), } } } #[derive(Clone, Debug, PartialEq)] pub struct ForkchoiceUpdatedResponse { pub payload_status: PayloadStatusV1, pub payload_id: Option, } #[derive(Clone, Copy, Debug, PartialEq)] pub enum ProposeBlindedBlockResponseStatus { Valid, Invalid, Syncing, } #[derive(Clone, Debug, PartialEq)] pub struct ProposeBlindedBlockResponse { pub status: ProposeBlindedBlockResponseStatus, pub latest_valid_hash: Option, pub validation_error: Option, } #[superstruct( variants(Bellatrix, Capella, Deneb, Electra, Fulu, Gloas), variant_attributes(derive(Clone, Debug, PartialEq),), map_into(ExecutionPayload), map_ref_into(ExecutionPayloadRef), cast_error(ty = "Error", expr = "Error::IncorrectStateVariant"), partial_getter_error(ty = "Error", expr = "Error::IncorrectStateVariant") )] #[derive(Clone, Debug, PartialEq)] pub struct GetPayloadResponse { #[superstruct( only(Bellatrix), partial_getter(rename = "execution_payload_bellatrix") )] pub execution_payload: ExecutionPayloadBellatrix, #[superstruct(only(Capella), partial_getter(rename = "execution_payload_capella"))] pub execution_payload: ExecutionPayloadCapella, #[superstruct(only(Deneb), partial_getter(rename = "execution_payload_deneb"))] pub execution_payload: ExecutionPayloadDeneb, #[superstruct(only(Electra), partial_getter(rename = "execution_payload_electra"))] pub execution_payload: ExecutionPayloadElectra, #[superstruct(only(Fulu), partial_getter(rename = "execution_payload_fulu"))] pub execution_payload: ExecutionPayloadFulu, #[superstruct(only(Gloas), partial_getter(rename = "execution_payload_gloas"))] pub execution_payload: ExecutionPayloadGloas, pub block_value: Uint256, #[superstruct(only(Deneb, Electra, Fulu, Gloas))] pub blobs_bundle: BlobsBundle, #[superstruct(only(Deneb, Electra, Fulu, Gloas), partial_getter(copy))] pub should_override_builder: bool, #[superstruct(only(Electra, Fulu, Gloas))] pub requests: ExecutionRequests, } impl GetPayloadResponse { pub fn fee_recipient(&self) -> Address { ExecutionPayloadRef::from(self.to_ref()).fee_recipient() } pub fn block_hash(&self) -> ExecutionBlockHash { ExecutionPayloadRef::from(self.to_ref()).block_hash() } pub fn block_number(&self) -> u64 { ExecutionPayloadRef::from(self.to_ref()).block_number() } } impl<'a, E: EthSpec> From> for ExecutionPayloadRef<'a, E> { fn from(response: GetPayloadResponseRef<'a, E>) -> Self { map_get_payload_response_ref_into_execution_payload_ref!(&'a _, response, |inner, cons| { cons(&inner.execution_payload) }) } } impl From> for ExecutionPayload { fn from(response: GetPayloadResponse) -> Self { map_get_payload_response_into_execution_payload!(response, |inner, cons| { cons(inner.execution_payload) }) } } impl From> for ( ExecutionPayload, Uint256, Option>, Option>, ) { fn from(response: GetPayloadResponse) -> Self { match response { GetPayloadResponse::Bellatrix(inner) => ( ExecutionPayload::Bellatrix(inner.execution_payload), inner.block_value, None, None, ), GetPayloadResponse::Capella(inner) => ( ExecutionPayload::Capella(inner.execution_payload), inner.block_value, None, None, ), GetPayloadResponse::Deneb(inner) => ( ExecutionPayload::Deneb(inner.execution_payload), inner.block_value, Some(inner.blobs_bundle), None, ), GetPayloadResponse::Electra(inner) => ( ExecutionPayload::Electra(inner.execution_payload), inner.block_value, Some(inner.blobs_bundle), Some(inner.requests), ), GetPayloadResponse::Fulu(inner) => ( ExecutionPayload::Fulu(inner.execution_payload), inner.block_value, Some(inner.blobs_bundle), Some(inner.requests), ), GetPayloadResponse::Gloas(inner) => ( ExecutionPayload::Gloas(inner.execution_payload), inner.block_value, Some(inner.blobs_bundle), Some(inner.requests), ), } } } pub enum GetPayloadResponseType { Full(GetPayloadResponse), Blinded(GetPayloadResponse), } impl GetPayloadResponse { pub fn execution_payload_ref(&self) -> ExecutionPayloadRef<'_, E> { self.to_ref().into() } } #[derive(Clone, Debug)] pub struct ExecutionPayloadBodyV1 { pub transactions: Transactions, pub withdrawals: Option>, } impl ExecutionPayloadBodyV1 { 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 )) } } } } } #[derive(Clone, Copy, Debug)] pub struct EngineCapabilities { pub new_payload_v1: bool, pub new_payload_v2: bool, pub new_payload_v3: bool, pub new_payload_v4: bool, pub forkchoice_updated_v1: bool, pub forkchoice_updated_v2: bool, pub forkchoice_updated_v3: bool, pub get_payload_bodies_by_hash_v1: bool, pub get_payload_bodies_by_range_v1: bool, pub get_payload_v1: bool, pub get_payload_v2: bool, pub get_payload_v3: bool, pub get_payload_v4: bool, pub get_payload_v5: bool, pub get_client_version_v1: bool, pub get_blobs_v1: bool, pub get_blobs_v2: bool, } impl EngineCapabilities { pub fn to_response(&self) -> Vec<&str> { let mut response = Vec::new(); if self.new_payload_v1 { response.push(ENGINE_NEW_PAYLOAD_V1); } if self.new_payload_v2 { response.push(ENGINE_NEW_PAYLOAD_V2); } if self.new_payload_v3 { response.push(ENGINE_NEW_PAYLOAD_V3); } if self.new_payload_v4 { response.push(ENGINE_NEW_PAYLOAD_V4); } if self.forkchoice_updated_v1 { response.push(ENGINE_FORKCHOICE_UPDATED_V1); } if self.forkchoice_updated_v2 { response.push(ENGINE_FORKCHOICE_UPDATED_V2); } if self.forkchoice_updated_v3 { response.push(ENGINE_FORKCHOICE_UPDATED_V3); } if self.get_payload_bodies_by_hash_v1 { response.push(ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1); } if self.get_payload_bodies_by_range_v1 { response.push(ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1); } if self.get_payload_v1 { response.push(ENGINE_GET_PAYLOAD_V1); } if self.get_payload_v2 { response.push(ENGINE_GET_PAYLOAD_V2); } if self.get_payload_v3 { response.push(ENGINE_GET_PAYLOAD_V3); } if self.get_payload_v4 { response.push(ENGINE_GET_PAYLOAD_V4); } if self.get_payload_v5 { response.push(ENGINE_GET_PAYLOAD_V5); } if self.get_client_version_v1 { response.push(ENGINE_GET_CLIENT_VERSION_V1); } if self.get_blobs_v1 { response.push(ENGINE_GET_BLOBS_V1); } if self.get_blobs_v2 { response.push(ENGINE_GET_BLOBS_V2); } response } } #[derive(Clone, Debug, PartialEq)] pub enum ClientCode { Besu, EtherumJS, Erigon, GoEthereum, Grandine, Lighthouse, Lodestar, Nethermind, Nimbus, TrinExecution, Teku, Prysm, Reth, Unknown(String), } impl std::fmt::Display for ClientCode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let s = match self { ClientCode::Besu => "BU", ClientCode::EtherumJS => "EJ", ClientCode::Erigon => "EG", ClientCode::GoEthereum => "GE", ClientCode::Grandine => "GR", ClientCode::Lighthouse => "LH", ClientCode::Lodestar => "LS", ClientCode::Nethermind => "NM", ClientCode::Nimbus => "NB", ClientCode::TrinExecution => "TE", ClientCode::Teku => "TK", ClientCode::Prysm => "PM", ClientCode::Reth => "RH", ClientCode::Unknown(code) => code, }; write!(f, "{}", s) } } impl TryFrom for ClientCode { type Error = String; fn try_from(code: String) -> Result { match code.as_str() { "BU" => Ok(Self::Besu), "EJ" => Ok(Self::EtherumJS), "EG" => Ok(Self::Erigon), "GE" => Ok(Self::GoEthereum), "GR" => Ok(Self::Grandine), "LH" => Ok(Self::Lighthouse), "LS" => Ok(Self::Lodestar), "NM" => Ok(Self::Nethermind), "NB" => Ok(Self::Nimbus), "TE" => Ok(Self::TrinExecution), "TK" => Ok(Self::Teku), "PM" => Ok(Self::Prysm), "RH" => Ok(Self::Reth), string => { if string.len() == 2 { Ok(Self::Unknown(code)) } else { Err(format!("Invalid client code: {}", code)) } } } } } #[derive(Clone, Debug)] pub struct CommitPrefix(pub String); impl TryFrom for CommitPrefix { type Error = String; fn try_from(value: String) -> Result { // Check if the input starts with '0x' and strip it if it does let commit_prefix = value.strip_prefix("0x").unwrap_or(&value); // Ensure length is exactly 8 characters after '0x' removal if commit_prefix.len() != 8 { return Err( "Input must be exactly 8 characters long (excluding any '0x' prefix)".to_string(), ); } // Ensure all characters are valid hex digits if commit_prefix.chars().all(|c| c.is_ascii_hexdigit()) { Ok(CommitPrefix(commit_prefix.to_lowercase())) } else { Err("Input must contain only hexadecimal characters".to_string()) } } } impl std::fmt::Display for CommitPrefix { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } #[derive(Clone, Debug)] pub struct ClientVersionV1 { pub code: ClientCode, pub name: String, pub version: String, pub commit: CommitPrefix, } impl ClientVersionV1 { pub fn calculate_graffiti(&self, lighthouse_commit_prefix: CommitPrefix) -> Graffiti { let graffiti_string = format!( "{}{}LH{}", self.code, self.commit .0 .get(..4) .map_or_else(|| self.commit.0.as_str(), |s| s) .to_lowercase(), lighthouse_commit_prefix .0 .get(..4) .unwrap_or("0000") .to_lowercase(), ); let mut graffiti_bytes = [0u8; GRAFFITI_BYTES_LEN]; let bytes_to_copy = std::cmp::min(graffiti_string.len(), GRAFFITI_BYTES_LEN); graffiti_bytes[..bytes_to_copy] .copy_from_slice(&graffiti_string.as_bytes()[..bytes_to_copy]); Graffiti::from(graffiti_bytes) } }