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