Skip to main content

ethrex_rpc/types/
payload.rs

1use bytes::Bytes;
2use ethrex_rlp::error::RLPDecodeError;
3use serde::{Deserialize, Serialize};
4
5use ethrex_common::{
6    Address, Bloom, H256, U256,
7    constants::DEFAULT_OMMERS_HASH,
8    serde_utils,
9    types::{
10        BlobsBundle, Block, BlockBody, BlockHash, BlockHeader, Transaction, Withdrawal,
11        block_access_list::BlockAccessList, compute_transactions_root, compute_withdrawals_root,
12        requests::EncodedRequests,
13    },
14};
15
16#[derive(Clone, Debug, Deserialize, Serialize)]
17#[serde(rename_all = "camelCase")]
18pub struct ExecutionPayload {
19    pub(crate) parent_hash: H256,
20    pub(crate) fee_recipient: Address,
21    pub(crate) state_root: H256,
22    pub(crate) receipts_root: H256,
23    pub(crate) logs_bloom: Bloom,
24    pub(crate) prev_randao: H256,
25    #[serde(with = "serde_utils::u64::hex_str")]
26    pub block_number: u64,
27    #[serde(with = "serde_utils::u64::hex_str")]
28    pub(crate) gas_limit: u64,
29    #[serde(with = "serde_utils::u64::hex_str")]
30    pub(crate) gas_used: u64,
31    #[serde(with = "serde_utils::u64::hex_str")]
32    pub timestamp: u64,
33    #[serde(with = "serde_utils::bytes")]
34    pub(crate) extra_data: Bytes,
35    #[serde(with = "serde_utils::u64::hex_str")]
36    pub(crate) base_fee_per_gas: u64,
37    pub block_hash: H256,
38    pub(crate) transactions: Vec<EncodedTransaction>,
39    #[serde(skip_serializing_if = "Option::is_none", default)]
40    pub withdrawals: Option<Vec<Withdrawal>>,
41    // ExecutionPayloadV3 fields. Optional since we support V2 too
42    #[serde(
43        skip_serializing_if = "Option::is_none",
44        with = "serde_utils::u64::hex_str_opt",
45        default
46    )]
47    pub blob_gas_used: Option<u64>,
48    #[serde(
49        skip_serializing_if = "Option::is_none",
50        with = "serde_utils::u64::hex_str_opt",
51        default
52    )]
53    pub excess_blob_gas: Option<u64>,
54    // ExecutionPayloadV4 fields (EIP-7843)
55    #[serde(
56        skip_serializing_if = "Option::is_none",
57        with = "serde_utils::u64::hex_str_opt",
58        default
59    )]
60    pub slot_number: Option<u64>,
61    // ExecutionPayloadV4 fields. Optional since we support previous versions.
62    #[serde(
63        skip_serializing_if = "Option::is_none",
64        with = "serde_utils::block_access_list::rlp_str_opt",
65        default
66    )]
67    pub block_access_list: Option<BlockAccessList>,
68}
69
70#[derive(Clone, Debug)]
71pub struct EncodedTransaction(pub Bytes);
72
73impl<'de> Deserialize<'de> for EncodedTransaction {
74    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
75    where
76        D: serde::Deserializer<'de>,
77    {
78        Ok(EncodedTransaction(serde_utils::bytes::deserialize(
79            deserializer,
80        )?))
81    }
82}
83
84impl Serialize for EncodedTransaction {
85    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
86    where
87        S: serde::Serializer,
88    {
89        serde_utils::bytes::serialize(&self.0, serializer)
90    }
91}
92
93impl EncodedTransaction {
94    /// Based on [EIP-2718]
95    /// Transactions can be encoded in the following formats:
96    /// 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)
97    /// B) `LegacyTransaction` (An rlp encoded LegacyTransaction)
98    fn decode(&self) -> Result<Transaction, RLPDecodeError> {
99        Transaction::decode_canonical(self.0.as_ref())
100    }
101
102    fn encode(tx: &Transaction) -> Self {
103        Self(Bytes::from(tx.encode_canonical_to_vec()))
104    }
105}
106
107impl ExecutionPayload {
108    /// Converts an `ExecutionPayload` into a block (aka a BlockHeader and BlockBody)
109    /// using the parentBeaconBlockRoot received along with the payload in the rpc call `engine_newPayloadV2/V3`
110    pub fn into_block(
111        self,
112        parent_beacon_block_root: Option<H256>,
113        requests_hash: Option<H256>,
114        block_access_list_hash: Option<H256>,
115    ) -> Result<Block, RLPDecodeError> {
116        let body = BlockBody {
117            transactions: self
118                .transactions
119                .iter()
120                .map(|encoded_tx| encoded_tx.decode())
121                .collect::<Result<Vec<_>, RLPDecodeError>>()?,
122            ommers: vec![],
123            withdrawals: self.withdrawals,
124        };
125        let header = BlockHeader {
126            parent_hash: self.parent_hash,
127            ommers_hash: *DEFAULT_OMMERS_HASH,
128            coinbase: self.fee_recipient,
129            state_root: self.state_root,
130            transactions_root: compute_transactions_root(
131                &body.transactions,
132                &ethrex_crypto::NativeCrypto,
133            ),
134            receipts_root: self.receipts_root,
135            logs_bloom: self.logs_bloom,
136            difficulty: 0.into(),
137            number: self.block_number,
138            gas_limit: self.gas_limit,
139            gas_used: self.gas_used,
140            timestamp: self.timestamp,
141            extra_data: self.extra_data,
142            prev_randao: self.prev_randao,
143            nonce: 0,
144            base_fee_per_gas: Some(self.base_fee_per_gas),
145            withdrawals_root: body
146                .withdrawals
147                .as_ref()
148                .map(|w| compute_withdrawals_root(w, &ethrex_crypto::NativeCrypto)),
149            blob_gas_used: self.blob_gas_used,
150            excess_blob_gas: self.excess_blob_gas,
151            parent_beacon_block_root,
152            // TODO: set the value properly
153            requests_hash,
154            slot_number: self.slot_number,
155            block_access_list_hash,
156            ..Default::default()
157        };
158
159        Ok(Block::new(header, body))
160    }
161
162    pub fn from_block(block: Block, block_access_list: Option<BlockAccessList>) -> Self {
163        Self {
164            parent_hash: block.header.parent_hash,
165            fee_recipient: block.header.coinbase,
166            state_root: block.header.state_root,
167            receipts_root: block.header.receipts_root,
168            logs_bloom: block.header.logs_bloom,
169            prev_randao: block.header.prev_randao,
170            block_number: block.header.number,
171            gas_limit: block.header.gas_limit,
172            gas_used: block.header.gas_used,
173            timestamp: block.header.timestamp,
174            extra_data: block.header.extra_data.clone(),
175            base_fee_per_gas: block.header.base_fee_per_gas.unwrap_or_default(),
176            block_hash: block.hash(),
177            transactions: block
178                .body
179                .transactions
180                .iter()
181                .map(EncodedTransaction::encode)
182                .collect(),
183            withdrawals: block.body.withdrawals,
184            blob_gas_used: block.header.blob_gas_used,
185            excess_blob_gas: block.header.excess_blob_gas,
186            slot_number: block.header.slot_number,
187            block_access_list,
188        }
189    }
190}
191
192#[derive(Debug, Deserialize, Serialize)]
193#[serde(rename_all = "camelCase")]
194pub struct PayloadStatus {
195    pub status: PayloadValidationStatus,
196    pub latest_valid_hash: Option<H256>,
197    pub validation_error: Option<String>,
198    #[serde(
199        default,
200        skip_serializing_if = "Option::is_none",
201        with = "optional_hex_bytes"
202    )]
203    pub witness: Option<Bytes>,
204}
205
206#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
207#[serde(rename_all = "UPPERCASE")]
208pub enum PayloadValidationStatus {
209    Valid,
210    Invalid,
211    Syncing,
212    Accepted,
213}
214
215impl PayloadStatus {
216    // Convenience methods to create payload status
217
218    pub fn invalid_with(latest_valid_hash: H256, error: String) -> Self {
219        PayloadStatus {
220            status: PayloadValidationStatus::Invalid,
221            latest_valid_hash: Some(latest_valid_hash),
222            validation_error: Some(error),
223            witness: None,
224        }
225    }
226
227    /// Creates a PayloadStatus with invalid status and error message
228    pub fn invalid_with_err(error: &str) -> Self {
229        PayloadStatus {
230            status: PayloadValidationStatus::Invalid,
231            latest_valid_hash: None,
232            validation_error: Some(error.to_string()),
233            witness: None,
234        }
235    }
236
237    /// Creates a PayloadStatus with invalid status and latest valid hash
238    pub fn invalid_with_hash(hash: BlockHash) -> Self {
239        PayloadStatus {
240            status: PayloadValidationStatus::Invalid,
241            latest_valid_hash: Some(hash),
242            validation_error: None,
243            witness: None,
244        }
245    }
246
247    /// Creates a PayloadStatus with syncing status and no other info
248    pub fn syncing() -> Self {
249        PayloadStatus {
250            status: PayloadValidationStatus::Syncing,
251            latest_valid_hash: None,
252            validation_error: None,
253            witness: None,
254        }
255    }
256
257    /// Creates a PayloadStatus with valid status and latest valid hash
258    pub fn valid_with_hash(hash: BlockHash) -> Self {
259        PayloadStatus {
260            status: PayloadValidationStatus::Valid,
261            latest_valid_hash: Some(hash),
262            validation_error: None,
263            witness: None,
264        }
265    }
266    /// Creates a PayloadStatus with valid status and latest valid hash
267    pub fn valid() -> Self {
268        PayloadStatus {
269            status: PayloadValidationStatus::Valid,
270            latest_valid_hash: None,
271            validation_error: None,
272            witness: None,
273        }
274    }
275}
276
277mod optional_hex_bytes {
278    use bytes::Bytes;
279    use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
280
281    pub fn serialize<S>(value: &Option<Bytes>, serializer: S) -> Result<S::Ok, S::Error>
282    where
283        S: Serializer,
284    {
285        let hex = value
286            .as_ref()
287            .map(|bytes| format!("0x{}", hex::encode(bytes)));
288        Option::<String>::serialize(&hex, serializer)
289    }
290
291    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Bytes>, D::Error>
292    where
293        D: Deserializer<'de>,
294    {
295        let value = Option::<String>::deserialize(deserializer)?;
296        match value {
297            Some(value) if !value.is_empty() => hex::decode(value.trim_start_matches("0x"))
298                .map(Bytes::from)
299                .map(Some)
300                .map_err(|error| D::Error::custom(error.to_string())),
301            _ => Ok(None),
302        }
303    }
304}
305
306#[derive(Clone, Debug, Serialize, Deserialize)]
307#[serde(rename_all = "camelCase")]
308pub struct ExecutionPayloadBody {
309    pub transactions: Vec<EncodedTransaction>,
310    pub withdrawals: Option<Vec<Withdrawal>>,
311}
312
313impl From<BlockBody> for ExecutionPayloadBody {
314    fn from(body: BlockBody) -> Self {
315        Self {
316            transactions: body
317                .transactions
318                .iter()
319                .map(EncodedTransaction::encode)
320                .collect(),
321            withdrawals: body.withdrawals,
322        }
323    }
324}
325
326/// ExecutionPayloadBody V2 - includes Block Access List for EIP-7928
327#[derive(Clone, Debug, Serialize, Deserialize)]
328#[serde(rename_all = "camelCase")]
329pub struct ExecutionPayloadBodyV2 {
330    pub transactions: Vec<EncodedTransaction>,
331    pub withdrawals: Option<Vec<Withdrawal>>,
332    #[serde(
333        skip_serializing_if = "Option::is_none",
334        with = "serde_utils::block_access_list::rlp_str_opt",
335        default
336    )]
337    pub block_access_list: Option<BlockAccessList>,
338}
339
340impl ExecutionPayloadBodyV2 {
341    pub fn from_body_with_bal(body: BlockBody, bal: Option<BlockAccessList>) -> Self {
342        Self {
343            transactions: body
344                .transactions
345                .iter()
346                .map(EncodedTransaction::encode)
347                .collect(),
348            withdrawals: body.withdrawals,
349            block_access_list: bal,
350        }
351    }
352}
353
354#[derive(Clone, Debug, Serialize, Deserialize)]
355#[serde(rename_all = "camelCase")]
356pub struct ExecutionPayloadResponse {
357    pub execution_payload: ExecutionPayload,
358    // Total fees consumed by the block (fees paid)
359    pub block_value: U256,
360    pub blobs_bundle: Option<BlobsBundle>,
361    pub should_override_builder: Option<bool>, // TODO: look into this
362    pub execution_requests: Option<Vec<EncodedRequests>>,
363}
364
365#[derive(Clone, Debug, Serialize, Deserialize)]
366#[serde(rename_all = "camelCase")]
367pub struct ExecutionPayloadResponseV2 {
368    pub execution_payload: ExecutionPayload,
369    // Total fees consumed by the block (fees paid)
370    pub block_value: U256,
371}
372
373#[cfg(test)]
374mod test {
375    use super::*;
376
377    #[test]
378    fn deserialize_payload_into_block() {
379        // Payload extracted from running kurtosis, only some transactions are included to reduce it's size.
380        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":[]}"#;
381        let payload: ExecutionPayload = serde_json::from_str(json).unwrap();
382        assert!(payload.into_block(Some(H256::zero()), None, None).is_ok());
383    }
384
385    #[test]
386    fn payload_status_omits_absent_witness() {
387        let status = PayloadStatus::valid_with_hash(H256::zero());
388        let json = serde_json::to_value(status).unwrap();
389
390        assert!(json.get("witness").is_none());
391    }
392
393    #[test]
394    fn payload_status_serializes_witness_as_hex() {
395        let mut status = PayloadStatus::valid_with_hash(H256::zero());
396        status.witness = Some(Bytes::from_static(&[0x12, 0x34]));
397
398        let json = serde_json::to_value(status).unwrap();
399
400        assert_eq!(json["witness"], "0x1234");
401    }
402}