reth_ethereum_engine_primitives/
payload.rs

1//! Contains types required for building a payload.
2
3use alloc::{sync::Arc, vec::Vec};
4use alloy_eips::{
5    eip4844::BlobTransactionSidecar,
6    eip4895::Withdrawals,
7    eip7594::{BlobTransactionSidecarEip7594, BlobTransactionSidecarVariant},
8    eip7685::Requests,
9};
10use alloy_primitives::{Address, B256, U256};
11use alloy_rlp::Encodable;
12use alloy_rpc_types_engine::{
13    BlobsBundleV1, BlobsBundleV2, ExecutionPayloadEnvelopeV2, ExecutionPayloadEnvelopeV3,
14    ExecutionPayloadEnvelopeV4, ExecutionPayloadEnvelopeV5, ExecutionPayloadFieldV2,
15    ExecutionPayloadV1, ExecutionPayloadV3, PayloadAttributes, PayloadId,
16};
17use core::convert::Infallible;
18use reth_ethereum_primitives::{Block, EthPrimitives};
19use reth_payload_primitives::{BuiltPayload, PayloadBuilderAttributes};
20use reth_primitives_traits::SealedBlock;
21
22use crate::BuiltPayloadConversionError;
23
24/// Contains the built payload.
25///
26/// According to the [engine API specification](https://github.com/ethereum/execution-apis/blob/main/src/engine/README.md) the execution layer should build the initial version of the payload with an empty transaction set and then keep update it in order to maximize the revenue.
27/// Therefore, the empty-block here is always available and full-block will be set/updated
28/// afterward.
29#[derive(Debug, Clone)]
30pub struct EthBuiltPayload {
31    /// Identifier of the payload
32    pub(crate) id: PayloadId,
33    /// The built block
34    pub(crate) block: Arc<SealedBlock<Block>>,
35    /// The fees of the block
36    pub(crate) fees: U256,
37    /// The blobs, proofs, and commitments in the block. If the block is pre-cancun, this will be
38    /// empty.
39    pub(crate) sidecars: BlobSidecars,
40    /// The requests of the payload
41    pub(crate) requests: Option<Requests>,
42}
43
44// === impl BuiltPayload ===
45
46impl EthBuiltPayload {
47    /// Initializes the payload with the given initial block
48    ///
49    /// Caution: This does not set any [`BlobSidecars`].
50    pub const fn new(
51        id: PayloadId,
52        block: Arc<SealedBlock<Block>>,
53        fees: U256,
54        requests: Option<Requests>,
55    ) -> Self {
56        Self { id, block, fees, requests, sidecars: BlobSidecars::Empty }
57    }
58
59    /// Returns the identifier of the payload.
60    pub const fn id(&self) -> PayloadId {
61        self.id
62    }
63
64    /// Returns the built block(sealed)
65    pub fn block(&self) -> &SealedBlock<Block> {
66        &self.block
67    }
68
69    /// Fees of the block
70    pub const fn fees(&self) -> U256 {
71        self.fees
72    }
73
74    /// Returns the blob sidecars.
75    pub const fn sidecars(&self) -> &BlobSidecars {
76        &self.sidecars
77    }
78
79    /// Sets blob transactions sidecars on the payload.
80    pub fn with_sidecars(mut self, sidecars: impl Into<BlobSidecars>) -> Self {
81        self.sidecars = sidecars.into();
82        self
83    }
84
85    /// Try converting built payload into [`ExecutionPayloadEnvelopeV3`].
86    ///
87    /// Returns an error if the payload contains non EIP-4844 sidecar.
88    pub fn try_into_v3(self) -> Result<ExecutionPayloadEnvelopeV3, BuiltPayloadConversionError> {
89        let Self { block, fees, sidecars, .. } = self;
90
91        let blobs_bundle = match sidecars {
92            BlobSidecars::Empty => BlobsBundleV1::empty(),
93            BlobSidecars::Eip4844(sidecars) => BlobsBundleV1::from(sidecars),
94            BlobSidecars::Eip7594(_) => {
95                return Err(BuiltPayloadConversionError::UnexpectedEip7594Sidecars)
96            }
97        };
98
99        Ok(ExecutionPayloadEnvelopeV3 {
100            execution_payload: ExecutionPayloadV3::from_block_unchecked(
101                block.hash(),
102                &Arc::unwrap_or_clone(block).into_block(),
103            ),
104            block_value: fees,
105            // From the engine API spec:
106            //
107            // > Client software **MAY** use any heuristics to decide whether to set
108            // `shouldOverrideBuilder` flag or not. If client software does not implement any
109            // heuristic this flag **SHOULD** be set to `false`.
110            //
111            // Spec:
112            // <https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/cancun.md#specification-2>
113            should_override_builder: false,
114            blobs_bundle,
115        })
116    }
117
118    /// Try converting built payload into [`ExecutionPayloadEnvelopeV4`].
119    ///
120    /// Returns an error if the payload contains non EIP-4844 sidecar.
121    pub fn try_into_v4(self) -> Result<ExecutionPayloadEnvelopeV4, BuiltPayloadConversionError> {
122        Ok(ExecutionPayloadEnvelopeV4 {
123            execution_requests: self.requests.clone().unwrap_or_default(),
124            envelope_inner: self.try_into()?,
125        })
126    }
127
128    /// Try converting built payload into [`ExecutionPayloadEnvelopeV5`].
129    pub fn try_into_v5(self) -> Result<ExecutionPayloadEnvelopeV5, BuiltPayloadConversionError> {
130        let Self { block, fees, sidecars, requests, .. } = self;
131
132        let blobs_bundle = match sidecars {
133            BlobSidecars::Empty => BlobsBundleV2::empty(),
134            BlobSidecars::Eip7594(sidecars) => BlobsBundleV2::from(sidecars),
135            BlobSidecars::Eip4844(_) => {
136                return Err(BuiltPayloadConversionError::UnexpectedEip4844Sidecars)
137            }
138        };
139
140        Ok(ExecutionPayloadEnvelopeV5 {
141            execution_payload: ExecutionPayloadV3::from_block_unchecked(
142                block.hash(),
143                &Arc::unwrap_or_clone(block).into_block(),
144            ),
145            block_value: fees,
146            // From the engine API spec:
147            //
148            // > Client software **MAY** use any heuristics to decide whether to set
149            // `shouldOverrideBuilder` flag or not. If client software does not implement any
150            // heuristic this flag **SHOULD** be set to `false`.
151            //
152            // Spec:
153            // <https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/cancun.md#specification-2>
154            should_override_builder: false,
155            blobs_bundle,
156            execution_requests: requests.unwrap_or_default(),
157        })
158    }
159}
160
161impl BuiltPayload for EthBuiltPayload {
162    type Primitives = EthPrimitives;
163
164    fn block(&self) -> &SealedBlock<Block> {
165        &self.block
166    }
167
168    fn fees(&self) -> U256 {
169        self.fees
170    }
171
172    fn requests(&self) -> Option<Requests> {
173        self.requests.clone()
174    }
175}
176
177// V1 engine_getPayloadV1 response
178impl From<EthBuiltPayload> for ExecutionPayloadV1 {
179    fn from(value: EthBuiltPayload) -> Self {
180        Self::from_block_unchecked(
181            value.block().hash(),
182            &Arc::unwrap_or_clone(value.block).into_block(),
183        )
184    }
185}
186
187// V2 engine_getPayloadV2 response
188impl From<EthBuiltPayload> for ExecutionPayloadEnvelopeV2 {
189    fn from(value: EthBuiltPayload) -> Self {
190        let EthBuiltPayload { block, fees, .. } = value;
191
192        Self {
193            block_value: fees,
194            execution_payload: ExecutionPayloadFieldV2::from_block_unchecked(
195                block.hash(),
196                &Arc::unwrap_or_clone(block).into_block(),
197            ),
198        }
199    }
200}
201
202impl TryFrom<EthBuiltPayload> for ExecutionPayloadEnvelopeV3 {
203    type Error = BuiltPayloadConversionError;
204
205    fn try_from(value: EthBuiltPayload) -> Result<Self, Self::Error> {
206        value.try_into_v3()
207    }
208}
209
210impl TryFrom<EthBuiltPayload> for ExecutionPayloadEnvelopeV4 {
211    type Error = BuiltPayloadConversionError;
212
213    fn try_from(value: EthBuiltPayload) -> Result<Self, Self::Error> {
214        value.try_into_v4()
215    }
216}
217
218impl TryFrom<EthBuiltPayload> for ExecutionPayloadEnvelopeV5 {
219    type Error = BuiltPayloadConversionError;
220
221    fn try_from(value: EthBuiltPayload) -> Result<Self, Self::Error> {
222        value.try_into_v5()
223    }
224}
225
226/// An enum representing blob transaction sidecars belonging to [`EthBuiltPayload`].
227#[derive(Clone, Default, Debug)]
228pub enum BlobSidecars {
229    /// No sidecars (default).
230    #[default]
231    Empty,
232    /// EIP-4844 style sidecars.
233    Eip4844(Vec<BlobTransactionSidecar>),
234    /// EIP-7594 style sidecars.
235    Eip7594(Vec<BlobTransactionSidecarEip7594>),
236}
237
238impl BlobSidecars {
239    /// Create new EIP-4844 style sidecars.
240    pub const fn eip4844(sidecars: Vec<BlobTransactionSidecar>) -> Self {
241        Self::Eip4844(sidecars)
242    }
243
244    /// Create new EIP-7594 style sidecars.
245    pub const fn eip7594(sidecars: Vec<BlobTransactionSidecarEip7594>) -> Self {
246        Self::Eip7594(sidecars)
247    }
248
249    /// Push EIP-4844 blob sidecar. Ignores the item if sidecars already contain EIP-7594 sidecars.
250    pub fn push_eip4844_sidecar(&mut self, sidecar: BlobTransactionSidecar) {
251        match self {
252            Self::Empty => {
253                *self = Self::Eip4844(Vec::from([sidecar]));
254            }
255            Self::Eip4844(sidecars) => {
256                sidecars.push(sidecar);
257            }
258            Self::Eip7594(_) => {}
259        }
260    }
261
262    /// Push EIP-7594 blob sidecar. Ignores the item if sidecars already contain EIP-4844 sidecars.
263    pub fn push_eip7594_sidecar(&mut self, sidecar: BlobTransactionSidecarEip7594) {
264        match self {
265            Self::Empty => {
266                *self = Self::Eip7594(Vec::from([sidecar]));
267            }
268            Self::Eip7594(sidecars) => {
269                sidecars.push(sidecar);
270            }
271            Self::Eip4844(_) => {}
272        }
273    }
274
275    /// Push a [`BlobTransactionSidecarVariant`]. Ignores the item if sidecars already contain the
276    /// opposite type.
277    pub fn push_sidecar_variant(&mut self, sidecar: BlobTransactionSidecarVariant) {
278        match sidecar {
279            BlobTransactionSidecarVariant::Eip4844(sidecar) => {
280                self.push_eip4844_sidecar(sidecar);
281            }
282            BlobTransactionSidecarVariant::Eip7594(sidecar) => {
283                self.push_eip7594_sidecar(sidecar);
284            }
285        }
286    }
287}
288
289impl From<Vec<BlobTransactionSidecar>> for BlobSidecars {
290    fn from(value: Vec<BlobTransactionSidecar>) -> Self {
291        Self::eip4844(value)
292    }
293}
294
295impl From<Vec<BlobTransactionSidecarEip7594>> for BlobSidecars {
296    fn from(value: Vec<BlobTransactionSidecarEip7594>) -> Self {
297        Self::eip7594(value)
298    }
299}
300
301impl From<alloc::vec::IntoIter<BlobTransactionSidecar>> for BlobSidecars {
302    fn from(value: alloc::vec::IntoIter<BlobTransactionSidecar>) -> Self {
303        value.collect::<Vec<_>>().into()
304    }
305}
306
307impl From<alloc::vec::IntoIter<BlobTransactionSidecarEip7594>> for BlobSidecars {
308    fn from(value: alloc::vec::IntoIter<BlobTransactionSidecarEip7594>) -> Self {
309        value.collect::<Vec<_>>().into()
310    }
311}
312
313/// Container type for all components required to build a payload.
314#[derive(Debug, Clone, PartialEq, Eq, Default)]
315pub struct EthPayloadBuilderAttributes {
316    /// Id of the payload
317    pub id: PayloadId,
318    /// Parent block to build the payload on top
319    pub parent: B256,
320    /// Unix timestamp for the generated payload
321    ///
322    /// Number of seconds since the Unix epoch.
323    pub timestamp: u64,
324    /// Address of the recipient for collecting transaction fee
325    pub suggested_fee_recipient: Address,
326    /// Randomness value for the generated payload
327    pub prev_randao: B256,
328    /// Withdrawals for the generated payload
329    pub withdrawals: Withdrawals,
330    /// Root of the parent beacon block
331    pub parent_beacon_block_root: Option<B256>,
332}
333
334// === impl EthPayloadBuilderAttributes ===
335
336impl EthPayloadBuilderAttributes {
337    /// Returns the identifier of the payload.
338    pub const fn payload_id(&self) -> PayloadId {
339        self.id
340    }
341
342    /// Creates a new payload builder for the given parent block and the attributes.
343    ///
344    /// Derives the unique [`PayloadId`] for the given parent and attributes
345    pub fn new(parent: B256, attributes: PayloadAttributes) -> Self {
346        let id = payload_id(&parent, &attributes);
347
348        Self {
349            id,
350            parent,
351            timestamp: attributes.timestamp,
352            suggested_fee_recipient: attributes.suggested_fee_recipient,
353            prev_randao: attributes.prev_randao,
354            withdrawals: attributes.withdrawals.unwrap_or_default().into(),
355            parent_beacon_block_root: attributes.parent_beacon_block_root,
356        }
357    }
358}
359
360impl PayloadBuilderAttributes for EthPayloadBuilderAttributes {
361    type RpcPayloadAttributes = PayloadAttributes;
362    type Error = Infallible;
363
364    /// Creates a new payload builder for the given parent block and the attributes.
365    ///
366    /// Derives the unique [`PayloadId`] for the given parent and attributes
367    fn try_new(
368        parent: B256,
369        attributes: PayloadAttributes,
370        _version: u8,
371    ) -> Result<Self, Infallible> {
372        Ok(Self::new(parent, attributes))
373    }
374
375    fn payload_id(&self) -> PayloadId {
376        self.id
377    }
378
379    fn parent(&self) -> B256 {
380        self.parent
381    }
382
383    fn timestamp(&self) -> u64 {
384        self.timestamp
385    }
386
387    fn parent_beacon_block_root(&self) -> Option<B256> {
388        self.parent_beacon_block_root
389    }
390
391    fn suggested_fee_recipient(&self) -> Address {
392        self.suggested_fee_recipient
393    }
394
395    fn prev_randao(&self) -> B256 {
396        self.prev_randao
397    }
398
399    fn withdrawals(&self) -> &Withdrawals {
400        &self.withdrawals
401    }
402}
403
404/// Generates the payload id for the configured payload from the [`PayloadAttributes`].
405///
406/// Returns an 8-byte identifier by hashing the payload components with sha256 hash.
407pub fn payload_id(parent: &B256, attributes: &PayloadAttributes) -> PayloadId {
408    use sha2::Digest;
409    let mut hasher = sha2::Sha256::new();
410    hasher.update(parent.as_slice());
411    hasher.update(&attributes.timestamp.to_be_bytes()[..]);
412    hasher.update(attributes.prev_randao.as_slice());
413    hasher.update(attributes.suggested_fee_recipient.as_slice());
414    if let Some(withdrawals) = &attributes.withdrawals {
415        let mut buf = Vec::new();
416        withdrawals.encode(&mut buf);
417        hasher.update(buf);
418    }
419
420    if let Some(parent_beacon_block) = attributes.parent_beacon_block_root {
421        hasher.update(parent_beacon_block);
422    }
423
424    let out = hasher.finalize();
425    PayloadId::new(out.as_slice()[..8].try_into().expect("sufficient length"))
426}
427
428#[cfg(test)]
429mod tests {
430    use super::*;
431    use alloy_eips::eip4895::Withdrawal;
432    use alloy_primitives::B64;
433    use core::str::FromStr;
434
435    #[test]
436    fn attributes_serde() {
437        let attributes = r#"{"timestamp":"0x1235","prevRandao":"0xf343b00e02dc34ec0124241f74f32191be28fb370bb48060f5fa4df99bda774c","suggestedFeeRecipient":"0x0000000000000000000000000000000000000000","withdrawals":null,"parentBeaconBlockRoot":null}"#;
438        let _attributes: PayloadAttributes = serde_json::from_str(attributes).unwrap();
439    }
440
441    #[test]
442    fn test_payload_id_basic() {
443        // Create a parent block and payload attributes
444        let parent =
445            B256::from_str("0x3b8fb240d288781d4aac94d3fd16809ee413bc99294a085798a589dae51ddd4a")
446                .unwrap();
447        let attributes = PayloadAttributes {
448            timestamp: 0x5,
449            prev_randao: B256::from_str(
450                "0x0000000000000000000000000000000000000000000000000000000000000000",
451            )
452            .unwrap(),
453            suggested_fee_recipient: Address::from_str(
454                "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b",
455            )
456            .unwrap(),
457            withdrawals: None,
458            parent_beacon_block_root: None,
459        };
460
461        // Verify that the generated payload ID matches the expected value
462        assert_eq!(
463            payload_id(&parent, &attributes),
464            PayloadId(B64::from_str("0xa247243752eb10b4").unwrap())
465        );
466    }
467
468    #[test]
469    fn test_payload_id_with_withdrawals() {
470        // Set up the parent and attributes with withdrawals
471        let parent =
472            B256::from_str("0x9876543210abcdef9876543210abcdef9876543210abcdef9876543210abcdef")
473                .unwrap();
474        let attributes = PayloadAttributes {
475            timestamp: 1622553200,
476            prev_randao: B256::from_slice(&[1; 32]),
477            suggested_fee_recipient: Address::from_str(
478                "0xb94f5374fce5edbc8e2a8697c15331677e6ebf0b",
479            )
480            .unwrap(),
481            withdrawals: Some(vec![
482                Withdrawal {
483                    index: 1,
484                    validator_index: 123,
485                    address: Address::from([0xAA; 20]),
486                    amount: 10,
487                },
488                Withdrawal {
489                    index: 2,
490                    validator_index: 456,
491                    address: Address::from([0xBB; 20]),
492                    amount: 20,
493                },
494            ]),
495            parent_beacon_block_root: None,
496        };
497
498        // Verify that the generated payload ID matches the expected value
499        assert_eq!(
500            payload_id(&parent, &attributes),
501            PayloadId(B64::from_str("0xedddc2f84ba59865").unwrap())
502        );
503    }
504
505    #[test]
506    fn test_payload_id_with_parent_beacon_block_root() {
507        // Set up the parent and attributes with a parent beacon block root
508        let parent =
509            B256::from_str("0x9876543210abcdef9876543210abcdef9876543210abcdef9876543210abcdef")
510                .unwrap();
511        let attributes = PayloadAttributes {
512            timestamp: 1622553200,
513            prev_randao: B256::from_str(
514                "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234",
515            )
516            .unwrap(),
517            suggested_fee_recipient: Address::from_str(
518                "0xc94f5374fce5edbc8e2a8697c15331677e6ebf0b",
519            )
520            .unwrap(),
521            withdrawals: None,
522            parent_beacon_block_root: Some(
523                B256::from_str(
524                    "0x2222222222222222222222222222222222222222222222222222222222222222",
525                )
526                .unwrap(),
527            ),
528        };
529
530        // Verify that the generated payload ID matches the expected value
531        assert_eq!(
532            payload_id(&parent, &attributes),
533            PayloadId(B64::from_str("0x0fc49cd532094cce").unwrap())
534        );
535    }
536}