use bytes::Bytes;
use ethrex_rlp::error::RLPDecodeError;
use serde::{Deserialize, Serialize};
use ethrex_common::{
Address, Bloom, H256, U256,
constants::DEFAULT_OMMERS_HASH,
serde_utils,
types::{
BlobsBundle, Block, BlockBody, BlockHash, BlockHeader, Transaction, Withdrawal,
block_access_list::BlockAccessList, compute_transactions_root, compute_withdrawals_root,
requests::EncodedRequests,
},
};
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecutionPayload {
pub(crate) parent_hash: H256,
pub(crate) fee_recipient: Address,
pub(crate) state_root: H256,
pub(crate) receipts_root: H256,
pub(crate) logs_bloom: Bloom,
pub(crate) prev_randao: H256,
#[serde(with = "serde_utils::u64::hex_str")]
pub block_number: u64,
#[serde(with = "serde_utils::u64::hex_str")]
pub(crate) gas_limit: u64,
#[serde(with = "serde_utils::u64::hex_str")]
pub(crate) gas_used: u64,
#[serde(with = "serde_utils::u64::hex_str")]
pub timestamp: u64,
#[serde(with = "serde_utils::bytes")]
pub(crate) extra_data: Bytes,
#[serde(with = "serde_utils::u64::hex_str")]
pub(crate) base_fee_per_gas: u64,
pub block_hash: H256,
pub(crate) transactions: Vec<EncodedTransaction>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub withdrawals: Option<Vec<Withdrawal>>,
// ExecutionPayloadV3 fields. Optional since we support V2 too
#[serde(
skip_serializing_if = "Option::is_none",
with = "serde_utils::u64::hex_str_opt",
default
)]
pub blob_gas_used: Option<u64>,
#[serde(
skip_serializing_if = "Option::is_none",
with = "serde_utils::u64::hex_str_opt",
default
)]
pub excess_blob_gas: Option<u64>,
// ExecutionPayloadV4 fields (EIP-7843)
#[serde(
skip_serializing_if = "Option::is_none",
with = "serde_utils::u64::hex_str_opt",
default
)]
pub slot_number: Option<u64>,
// ExecutionPayloadV4 fields. Optional since we support previous versions.
#[serde(
skip_serializing_if = "Option::is_none",
with = "serde_utils::block_access_list::rlp_str_opt",
default
)]
pub block_access_list: Option<BlockAccessList>,
}
#[derive(Clone, Debug)]
pub struct EncodedTransaction(pub Bytes);
impl<'de> Deserialize<'de> for EncodedTransaction {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(EncodedTransaction(serde_utils::bytes::deserialize(
deserializer,
)?))
}
}
impl Serialize for EncodedTransaction {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serde_utils::bytes::serialize(&self.0, serializer)
}
}
impl EncodedTransaction {
/// Based on [EIP-2718]
/// Transactions can be encoded in the following formats:
/// A) `TransactionType || Transaction` (Where Transaction type is an 8-bit number between 0 and 0x7f, and Transaction is an rlp encoded transaction of type TransactionType)
/// B) `LegacyTransaction` (An rlp encoded LegacyTransaction)
fn decode(&self) -> Result<Transaction, RLPDecodeError> {
Transaction::decode_canonical(self.0.as_ref())
}
fn encode(tx: &Transaction) -> Self {
Self(Bytes::from(tx.encode_canonical_to_vec()))
}
}
impl ExecutionPayload {
/// Converts an `ExecutionPayload` into a block (aka a BlockHeader and BlockBody)
/// using the parentBeaconBlockRoot received along with the payload in the rpc call `engine_newPayloadV2/V3`
pub fn into_block(
self,
parent_beacon_block_root: Option<H256>,
requests_hash: Option<H256>,
block_access_list_hash: Option<H256>,
) -> Result<Block, RLPDecodeError> {
let body = BlockBody {
transactions: self
.transactions
.iter()
.map(|encoded_tx| encoded_tx.decode())
.collect::<Result<Vec<_>, RLPDecodeError>>()?,
ommers: vec![],
withdrawals: self.withdrawals,
};
let header = BlockHeader {
parent_hash: self.parent_hash,
ommers_hash: *DEFAULT_OMMERS_HASH,
coinbase: self.fee_recipient,
state_root: self.state_root,
transactions_root: compute_transactions_root(
&body.transactions,
ðrex_crypto::NativeCrypto,
),
receipts_root: self.receipts_root,
logs_bloom: self.logs_bloom,
difficulty: 0.into(),
number: self.block_number,
gas_limit: self.gas_limit,
gas_used: self.gas_used,
timestamp: self.timestamp,
extra_data: self.extra_data,
prev_randao: self.prev_randao,
nonce: 0,
base_fee_per_gas: Some(self.base_fee_per_gas),
withdrawals_root: body
.withdrawals
.as_ref()
.map(|w| compute_withdrawals_root(w, ðrex_crypto::NativeCrypto)),
blob_gas_used: self.blob_gas_used,
excess_blob_gas: self.excess_blob_gas,
parent_beacon_block_root,
// TODO: set the value properly
requests_hash,
slot_number: self.slot_number,
block_access_list_hash,
..Default::default()
};
Ok(Block::new(header, body))
}
pub fn from_block(block: Block, block_access_list: Option<BlockAccessList>) -> Self {
Self {
parent_hash: block.header.parent_hash,
fee_recipient: block.header.coinbase,
state_root: block.header.state_root,
receipts_root: block.header.receipts_root,
logs_bloom: block.header.logs_bloom,
prev_randao: block.header.prev_randao,
block_number: block.header.number,
gas_limit: block.header.gas_limit,
gas_used: block.header.gas_used,
timestamp: block.header.timestamp,
extra_data: block.header.extra_data.clone(),
base_fee_per_gas: block.header.base_fee_per_gas.unwrap_or_default(),
block_hash: block.hash(),
transactions: block
.body
.transactions
.iter()
.map(EncodedTransaction::encode)
.collect(),
withdrawals: block.body.withdrawals,
blob_gas_used: block.header.blob_gas_used,
excess_blob_gas: block.header.excess_blob_gas,
slot_number: block.header.slot_number,
block_access_list,
}
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PayloadStatus {
pub status: PayloadValidationStatus,
pub latest_valid_hash: Option<H256>,
pub validation_error: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "optional_hex_bytes"
)]
pub witness: Option<Bytes>,
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "UPPERCASE")]
pub enum PayloadValidationStatus {
Valid,
Invalid,
Syncing,
Accepted,
}
impl PayloadStatus {
// Convenience methods to create payload status
pub fn invalid_with(latest_valid_hash: H256, error: String) -> Self {
PayloadStatus {
status: PayloadValidationStatus::Invalid,
latest_valid_hash: Some(latest_valid_hash),
validation_error: Some(error),
witness: None,
}
}
/// Creates a PayloadStatus with invalid status and error message
pub fn invalid_with_err(error: &str) -> Self {
PayloadStatus {
status: PayloadValidationStatus::Invalid,
latest_valid_hash: None,
validation_error: Some(error.to_string()),
witness: None,
}
}
/// Creates a PayloadStatus with invalid status and latest valid hash
pub fn invalid_with_hash(hash: BlockHash) -> Self {
PayloadStatus {
status: PayloadValidationStatus::Invalid,
latest_valid_hash: Some(hash),
validation_error: None,
witness: None,
}
}
/// Creates a PayloadStatus with syncing status and no other info
pub fn syncing() -> Self {
PayloadStatus {
status: PayloadValidationStatus::Syncing,
latest_valid_hash: None,
validation_error: None,
witness: None,
}
}
/// Creates a PayloadStatus with valid status and latest valid hash
pub fn valid_with_hash(hash: BlockHash) -> Self {
PayloadStatus {
status: PayloadValidationStatus::Valid,
latest_valid_hash: Some(hash),
validation_error: None,
witness: None,
}
}
/// Creates a PayloadStatus with valid status and latest valid hash
pub fn valid() -> Self {
PayloadStatus {
status: PayloadValidationStatus::Valid,
latest_valid_hash: None,
validation_error: None,
witness: None,
}
}
}
mod optional_hex_bytes {
use bytes::Bytes;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
pub fn serialize<S>(value: &Option<Bytes>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let hex = value
.as_ref()
.map(|bytes| format!("0x{}", hex::encode(bytes)));
Option::<String>::serialize(&hex, serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Bytes>, D::Error>
where
D: Deserializer<'de>,
{
let value = Option::<String>::deserialize(deserializer)?;
match value {
Some(value) if !value.is_empty() => hex::decode(value.trim_start_matches("0x"))
.map(Bytes::from)
.map(Some)
.map_err(|error| D::Error::custom(error.to_string())),
_ => Ok(None),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecutionPayloadBody {
pub transactions: Vec<EncodedTransaction>,
pub withdrawals: Option<Vec<Withdrawal>>,
}
impl From<BlockBody> for ExecutionPayloadBody {
fn from(body: BlockBody) -> Self {
Self {
transactions: body
.transactions
.iter()
.map(EncodedTransaction::encode)
.collect(),
withdrawals: body.withdrawals,
}
}
}
/// ExecutionPayloadBody V2 - includes Block Access List for EIP-7928
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecutionPayloadBodyV2 {
pub transactions: Vec<EncodedTransaction>,
pub withdrawals: Option<Vec<Withdrawal>>,
#[serde(
skip_serializing_if = "Option::is_none",
with = "serde_utils::block_access_list::rlp_str_opt",
default
)]
pub block_access_list: Option<BlockAccessList>,
}
impl ExecutionPayloadBodyV2 {
pub fn from_body_with_bal(body: BlockBody, bal: Option<BlockAccessList>) -> Self {
Self {
transactions: body
.transactions
.iter()
.map(EncodedTransaction::encode)
.collect(),
withdrawals: body.withdrawals,
block_access_list: bal,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecutionPayloadResponse {
pub execution_payload: ExecutionPayload,
// Total fees consumed by the block (fees paid)
pub block_value: U256,
pub blobs_bundle: Option<BlobsBundle>,
pub should_override_builder: Option<bool>, // TODO: look into this
pub execution_requests: Option<Vec<EncodedRequests>>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecutionPayloadResponseV2 {
pub execution_payload: ExecutionPayload,
// Total fees consumed by the block (fees paid)
pub block_value: U256,
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn deserialize_payload_into_block() {
// Payload extracted from running kurtosis, only some transactions are included to reduce it's size.
let json = r#"{"baseFeePerGas":"0x342770c0","blobGasUsed":"0x0","blockHash":"0x4029a2342bb6d54db91457bc8e442be22b3481df8edea24cc721f9d0649f65be","blockNumber":"0x1","excessBlobGas":"0x0","extraData":"0xd883010e06846765746888676f312e32322e34856c696e7578","feeRecipient":"0x8943545177806ed17b9f23f0a21ee5948ecaa776","gasLimit":"0x17dd79d","gasUsed":"0x401640","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","parentHash":"0x2971eefd1f71f3548728cad87c16cc91b979ef035054828c59a02e49ae300a84","prevRandao":"0x2971eefd1f71f3548728cad87c16cc91b979ef035054828c59a02e49ae300a84","receiptsRoot":"0x0185e8473b81c3a504c4919249a94a94965a2f61c06367ee6ffb88cb7a3ef02b","stateRoot":"0x0eb8fd0af53174e65bb660d0904e5016425a713d8f11c767c26148b526fc05f3","timestamp":"0x66846fb2","transactions":["0xf86d80843baa0c4082f618946177843db3138ae69679a54b95cf345ed759450d870aa87bee538000808360306ba0151ccc02146b9b11adf516e6787b59acae3e76544fdcd75e77e67c6b598ce65da064c5dd5aae2fbb535830ebbdad0234975cd7ece3562013b63ea18cc0df6c97d4","0xf86d01843baa0c4082f61894687704db07e902e9a8b3754031d168d46e3d586e870aa87bee538000808360306ba0f6c479c3e9135a61d7cca17b7354ddc311cda2d8df265d0378f940bdefd62b54a077786891b0b6bcd438d8c24d00fa6628bc2f1caa554f9dec0a96daa4f40eb0d7","0xf86d02843baa0c4082f6189415e6a5a2e131dd5467fa1ff3acd104f45ee5940b870aa87bee538000808360306ca084469ec8ee41e9104cbe3ad7e7fe4225de86076dd2783749b099a4d155900305a07e64e8848c692f0fc251e78e6f3c388eb303349f3e247481366517c2a5ae2d89","0xf86d03843baa0c4082f6189480c4c7125967139acaa931ee984a9db4100e0f3b870aa87bee538000808360306ba021d2d8a35b8da03d7e0b494f71c9ed1c28a195b94c298407b81d65163a79fbdaa024a9bfcf5bbe75ba35130fa784ab88cd21c12c4e7daf3464de91bc1ed07d1bf6","0xf86d04843baa0c4082f61894d08a63244fcd28b0aec5075052cdce31ba04fead870aa87bee538000808360306ca07ee42fee5e426595056ad406aa65a3c7adb1d3d77279f56ebe2410bcf5118b2ca07b8a0e1d21578e9043a7331f60bafc71d15788d1a2d70d00b3c46e0856ff56d2","0xf86d05843baa0c4082f618940b06ef8be65fcda88f2dbae5813480f997ee8e35870aa87bee538000808360306ba0620669c8d6a781d3131bca874152bf833622af0edcd2247eab1b086875d5242ba01632353388f46946b5ce037130e92128e5837fe35d6c7de2b9e56a0f8cc1f5e6", "0x02f8ef83301824048413f157f8842daf517a830186a094000000000000000000000000000000000000000080b8807a0a600060a0553db8600060c855c77fb29ecd7661d8aefe101a0db652a728af0fded622ff55d019b545d03a7532932a60ad52604260cd5360bf60ce53609460cf53603e60d05360f560d153bc596000609e55600060c6556000601f556000609155535660556057536055605853606e60595360e7605a5360d0605b5360eb60c080a03acb03b1fc20507bc66210f7e18ff5af65038fb22c626ae488ad9513d9b6debca05d38459e9d2a221eb345b0c2761b719b313d062ff1ea3d10cf5b8762c44385a6"],"withdrawals":[]}"#;
let payload: ExecutionPayload = serde_json::from_str(json).unwrap();
assert!(payload.into_block(Some(H256::zero()), None, None).is_ok());
}
#[test]
fn payload_status_omits_absent_witness() {
let status = PayloadStatus::valid_with_hash(H256::zero());
let json = serde_json::to_value(status).unwrap();
assert!(json.get("witness").is_none());
}
#[test]
fn payload_status_serializes_witness_as_hex() {
let mut status = PayloadStatus::valid_with_hash(H256::zero());
status.witness = Some(Bytes::from_static(&[0x12, 0x34]));
let json = serde_json::to_value(status).unwrap();
assert_eq!(json["witness"], "0x1234");
}
}