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