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