Skip to main content

reth_payload_primitives/
traits.rs

1//! Core traits for working with execution payloads.
2
3use crate::PayloadBuilderError;
4use alloc::{boxed::Box, sync::Arc, vec::Vec};
5use alloy_eips::{eip4895::Withdrawal, eip7685::Requests};
6use alloy_primitives::{Bytes, B256, U256};
7use alloy_rlp::Encodable;
8use alloy_rpc_types_engine::{PayloadAttributes as EthPayloadAttributes, PayloadId};
9use core::fmt;
10use either::Either;
11use reth_execution_types::BlockExecutionOutput;
12use reth_primitives_traits::{NodePrimitives, RecoveredBlock, SealedBlock, SealedHeader};
13use reth_trie_common::{updates::TrieUpdates, HashedPostState};
14
15/// Represents an executed block for payload building purposes.
16///
17/// This type captures the complete execution state of a built block,
18/// including the recovered block, execution outcome, hashed state, and trie updates.
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct BuiltPayloadExecutedBlock<N: NodePrimitives> {
21    /// Recovered Block
22    pub recovered_block: Arc<RecoveredBlock<N::Block>>,
23    /// Block's execution outcome.
24    pub execution_output: Arc<BlockExecutionOutput<N::Receipt>>,
25    /// Block's hashed state (unsorted).
26    pub hashed_state: Arc<HashedPostState>,
27    /// Trie updates that result from calculating the state root for the block (unsorted).
28    pub trie_updates: Arc<TrieUpdates>,
29}
30
31/// Represents a successfully built execution payload (block).
32///
33/// Provides access to the underlying block data, execution results, and associated metadata
34/// for payloads ready for execution or propagation.
35#[auto_impl::auto_impl(&, Arc)]
36pub trait BuiltPayload: Send + Sync + fmt::Debug {
37    /// The node's primitive types
38    type Primitives: NodePrimitives;
39
40    /// Returns the built block in its sealed (hash-verified) form.
41    fn block(&self) -> &SealedBlock<<Self::Primitives as NodePrimitives>::Block>;
42
43    /// Returns the total fees collected from all transactions in this block.
44    fn fees(&self) -> U256;
45
46    /// Returns the EIP-7928 block access list included in this payload.
47    ///
48    /// Returns `None` for payloads that do not carry a block access list.
49    fn block_access_list(&self) -> Option<&Bytes> {
50        None
51    }
52
53    /// Returns the complete execution result including state updates.
54    ///
55    /// Returns `None` if execution data is not available or not tracked.
56    fn executed_block(&self) -> Option<BuiltPayloadExecutedBlock<Self::Primitives>> {
57        None
58    }
59
60    /// Returns the EIP-7685 execution layer requests included in this block.
61    ///
62    /// These are requests generated by the execution layer that need to be
63    /// processed by the consensus layer (e.g., validator deposits, withdrawals).
64    fn requests(&self) -> Option<Requests>;
65}
66
67/// Basic attributes required to initiate payload construction.
68///
69/// Defines minimal parameters needed to build a new execution payload.
70/// Implementations must be serializable for transmission.
71pub trait PayloadAttributes:
72    serde::de::DeserializeOwned + serde::Serialize + fmt::Debug + Clone + Send + Sync + 'static
73{
74    /// Computes the unique identifier for this payload build job.
75    fn payload_id(&self, parent_hash: &B256) -> PayloadId;
76
77    /// Returns the timestamp for the new payload.
78    fn timestamp(&self) -> u64;
79
80    /// Returns the withdrawals to be included in the payload.
81    ///
82    /// `Some` for post-Shanghai blocks, `None` for earlier blocks.
83    fn withdrawals(&self) -> Option<&Vec<Withdrawal>>;
84
85    /// Returns the parent beacon block root.
86    ///
87    /// `Some` for post-merge blocks, `None` for pre-merge blocks.
88    fn parent_beacon_block_root(&self) -> Option<B256>;
89
90    /// Returns the slot number for the new payload.
91    ///
92    /// `Some` for post-Amsterdam blocks, `None` for earlier blocks.
93    fn slot_number(&self) -> Option<u64>;
94
95    /// Returns the target gas limit for the new payload.
96    ///
97    /// `Some` for payload attributes that specify the desired gas limit, `None` if the builder
98    /// should use its configured target.
99    fn target_gas_limit(&self) -> Option<u64> {
100        None
101    }
102
103    /// Returns the EIP-7805 inclusion-list transactions supplied by the consensus layer.
104    fn inclusion_list_transactions(&self) -> Option<&[Bytes]> {
105        None
106    }
107}
108
109impl PayloadAttributes for EthPayloadAttributes {
110    fn payload_id(&self, parent_hash: &B256) -> PayloadId {
111        payload_id(parent_hash, self)
112    }
113
114    fn timestamp(&self) -> u64 {
115        self.timestamp
116    }
117
118    fn withdrawals(&self) -> Option<&Vec<Withdrawal>> {
119        self.withdrawals.as_ref()
120    }
121
122    fn parent_beacon_block_root(&self) -> Option<B256> {
123        self.parent_beacon_block_root
124    }
125
126    fn slot_number(&self) -> Option<u64> {
127        self.slot_number
128    }
129
130    fn target_gas_limit(&self) -> Option<u64> {
131        self.target_gas_limit
132    }
133}
134
135/// Factory trait for creating payload attributes.
136///
137/// Enables different strategies for generating payload attributes based on
138/// contextual information. Useful for testing and specialized building.
139pub trait PayloadAttributesBuilder<Attributes, Header = alloy_consensus::Header>:
140    Send + Sync + 'static
141{
142    /// Constructs new payload attributes for the given timestamp.
143    fn build(&self, parent: &SealedHeader<Header>) -> Attributes;
144}
145
146impl<Attributes, Header, F> PayloadAttributesBuilder<Attributes, Header> for F
147where
148    Header: Clone,
149    F: Fn(SealedHeader<Header>) -> Attributes + Send + Sync + 'static,
150{
151    fn build(&self, parent: &SealedHeader<Header>) -> Attributes {
152        self(parent.clone())
153    }
154}
155
156impl<Attributes, Header, L, R> PayloadAttributesBuilder<Attributes, Header> for Either<L, R>
157where
158    L: PayloadAttributesBuilder<Attributes, Header>,
159    R: PayloadAttributesBuilder<Attributes, Header>,
160{
161    fn build(&self, parent: &SealedHeader<Header>) -> Attributes {
162        match self {
163            Self::Left(l) => l.build(parent),
164            Self::Right(r) => r.build(parent),
165        }
166    }
167}
168
169impl<Attributes, Header> PayloadAttributesBuilder<Attributes, Header>
170    for Box<dyn PayloadAttributesBuilder<Attributes, Header>>
171where
172    Header: 'static,
173    Attributes: 'static,
174{
175    fn build(&self, parent: &SealedHeader<Header>) -> Attributes {
176        self.as_ref().build(parent)
177    }
178}
179
180/// Trait to build the EVM environment for the next block from the given payload attributes.
181///
182/// Accepts payload attributes from CL, parent header and additional payload builder context.
183pub trait BuildNextEnv<Attributes, Header, Ctx>: Sized {
184    /// Builds the EVM environment for the next block from the given payload attributes.
185    fn build_next_env(
186        attributes: &Attributes,
187        parent: &SealedHeader<Header>,
188        ctx: &Ctx,
189    ) -> Result<Self, PayloadBuilderError>;
190}
191
192/// Generates the payload id for the configured payload from the [`PayloadAttributes`].
193///
194/// Returns an 8-byte identifier by hashing the payload components with sha256 hash.
195pub fn payload_id(
196    parent: &B256,
197    attributes: &alloy_rpc_types_engine::PayloadAttributes,
198) -> PayloadId {
199    use sha2::Digest;
200    let mut hasher = sha2::Sha256::new();
201    hasher.update(parent.as_slice());
202    hasher.update(&attributes.timestamp.to_be_bytes()[..]);
203    hasher.update(attributes.prev_randao.as_slice());
204    hasher.update(attributes.suggested_fee_recipient.as_slice());
205    if let Some(withdrawals) = &attributes.withdrawals {
206        let mut buf = Vec::new();
207        withdrawals.encode(&mut buf);
208        hasher.update(buf);
209    }
210
211    if let Some(parent_beacon_block) = attributes.parent_beacon_block_root {
212        hasher.update(parent_beacon_block);
213    }
214
215    if let Some(slot_number) = attributes.slot_number {
216        hasher.update(slot_number.to_be_bytes());
217    }
218
219    if let Some(target_gas_limit) = attributes.target_gas_limit {
220        hasher.update(target_gas_limit.to_be_bytes());
221    }
222
223    let out = hasher.finalize();
224
225    #[allow(deprecated)] // generic-array 0.14 deprecated
226    PayloadId::new(out.as_slice()[..8].try_into().expect("sufficient length"))
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232    use alloy_eips::eip4895::Withdrawal;
233    use alloy_primitives::{Address, B64};
234    use core::str::FromStr;
235
236    #[test]
237    fn attributes_serde() {
238        let attributes = r#"{"timestamp":"0x1235","prevRandao":"0xf343b00e02dc34ec0124241f74f32191be28fb370bb48060f5fa4df99bda774c","suggestedFeeRecipient":"0x0000000000000000000000000000000000000000","withdrawals":null,"parentBeaconBlockRoot":null}"#;
239        let _attributes: EthPayloadAttributes = serde_json::from_str(attributes).unwrap();
240    }
241
242    #[test]
243    fn test_payload_id_basic() {
244        // Create a parent block and payload attributes
245        let parent =
246            B256::from_str("0x3b8fb240d288781d4aac94d3fd16809ee413bc99294a085798a589dae51ddd4a")
247                .unwrap();
248        let attributes = EthPayloadAttributes {
249            timestamp: 0x5,
250            prev_randao: B256::from_str(
251                "0x0000000000000000000000000000000000000000000000000000000000000000",
252            )
253            .unwrap(),
254            suggested_fee_recipient: Address::from_str(
255                "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b",
256            )
257            .unwrap(),
258            withdrawals: None,
259            parent_beacon_block_root: None,
260            slot_number: None,
261            ..Default::default()
262        };
263
264        // Verify that the generated payload ID matches the expected value
265        assert_eq!(
266            payload_id(&parent, &attributes),
267            PayloadId(B64::from_str("0xa247243752eb10b4").unwrap())
268        );
269    }
270
271    #[test]
272    fn test_payload_id_with_withdrawals() {
273        // Set up the parent and attributes with withdrawals
274        let parent =
275            B256::from_str("0x9876543210abcdef9876543210abcdef9876543210abcdef9876543210abcdef")
276                .unwrap();
277        let attributes = EthPayloadAttributes {
278            timestamp: 1622553200,
279            prev_randao: B256::from_slice(&[1; 32]),
280            suggested_fee_recipient: Address::from_str(
281                "0xb94f5374fce5edbc8e2a8697c15331677e6ebf0b",
282            )
283            .unwrap(),
284            withdrawals: Some(vec![
285                Withdrawal {
286                    index: 1,
287                    validator_index: 123,
288                    address: Address::from([0xAA; 20]),
289                    amount: 10,
290                },
291                Withdrawal {
292                    index: 2,
293                    validator_index: 456,
294                    address: Address::from([0xBB; 20]),
295                    amount: 20,
296                },
297            ]),
298            parent_beacon_block_root: None,
299            slot_number: None,
300            ..Default::default()
301        };
302
303        // Verify that the generated payload ID matches the expected value
304        assert_eq!(
305            payload_id(&parent, &attributes),
306            PayloadId(B64::from_str("0xedddc2f84ba59865").unwrap())
307        );
308    }
309
310    #[test]
311    fn test_payload_id_with_parent_beacon_block_root() {
312        // Set up the parent and attributes with a parent beacon block root
313        let parent =
314            B256::from_str("0x9876543210abcdef9876543210abcdef9876543210abcdef9876543210abcdef")
315                .unwrap();
316        let attributes = EthPayloadAttributes {
317            timestamp: 1622553200,
318            prev_randao: B256::from_str(
319                "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234",
320            )
321            .unwrap(),
322            suggested_fee_recipient: Address::from_str(
323                "0xc94f5374fce5edbc8e2a8697c15331677e6ebf0b",
324            )
325            .unwrap(),
326            withdrawals: None,
327            parent_beacon_block_root: Some(
328                B256::from_str(
329                    "0x2222222222222222222222222222222222222222222222222222222222222222",
330                )
331                .unwrap(),
332            ),
333            slot_number: None,
334            ..Default::default()
335        };
336
337        // Verify that the generated payload ID matches the expected value
338        assert_eq!(
339            payload_id(&parent, &attributes),
340            PayloadId(B64::from_str("0x0fc49cd532094cce").unwrap())
341        );
342    }
343
344    #[test]
345    fn test_payload_id_with_slot_number() {
346        let parent =
347            B256::from_str("0x9876543210abcdef9876543210abcdef9876543210abcdef9876543210abcdef")
348                .unwrap();
349        let mut attributes = EthPayloadAttributes {
350            timestamp: 1622553200,
351            prev_randao: B256::from_slice(&[1; 32]),
352            suggested_fee_recipient: Address::from_str(
353                "0xb94f5374fce5edbc8e2a8697c15331677e6ebf0b",
354            )
355            .unwrap(),
356            withdrawals: Some(vec![]),
357            parent_beacon_block_root: Some(B256::from_slice(&[2; 32])),
358            slot_number: Some(1),
359            ..Default::default()
360        };
361
362        let first = payload_id(&parent, &attributes);
363        attributes.slot_number = Some(2);
364
365        assert_ne!(first, payload_id(&parent, &attributes));
366    }
367
368    #[test]
369    fn test_payload_id_with_target_gas_limit() {
370        let parent =
371            B256::from_str("0x9876543210abcdef9876543210abcdef9876543210abcdef9876543210abcdef")
372                .unwrap();
373        #[allow(clippy::needless_update)]
374        let mut attributes = EthPayloadAttributes {
375            timestamp: 1622553200,
376            prev_randao: B256::from_slice(&[1; 32]),
377            suggested_fee_recipient: Address::from_str(
378                "0xb94f5374fce5edbc8e2a8697c15331677e6ebf0b",
379            )
380            .unwrap(),
381            withdrawals: Some(vec![]),
382            parent_beacon_block_root: Some(B256::from_slice(&[2; 32])),
383            slot_number: Some(1),
384            target_gas_limit: Some(30_000_000),
385            ..Default::default()
386        };
387
388        let first = payload_id(&parent, &attributes);
389        attributes.target_gas_limit = Some(60_000_000);
390
391        assert_ne!(first, payload_id(&parent, &attributes));
392    }
393}