reth_ethereum_engine_primitives/
payload.rs

1//! Contains types required for building a payload.
2
3use alloc::{sync::Arc, vec::Vec};
4use alloy_eips::{eip4844::BlobTransactionSidecar, eip4895::Withdrawals, eip7685::Requests};
5use alloy_primitives::{Address, B256, U256};
6use alloy_rlp::Encodable;
7use alloy_rpc_types_engine::{
8    ExecutionPayloadEnvelopeV2, ExecutionPayloadEnvelopeV3, ExecutionPayloadEnvelopeV4,
9    ExecutionPayloadFieldV2, ExecutionPayloadV1, ExecutionPayloadV3, PayloadAttributes, PayloadId,
10};
11use core::convert::Infallible;
12use reth_ethereum_primitives::{Block, EthPrimitives};
13use reth_payload_primitives::{BuiltPayload, PayloadBuilderAttributes};
14use reth_primitives_traits::SealedBlock;
15
16/// Contains the built payload.
17///
18/// 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.
19/// Therefore, the empty-block here is always available and full-block will be set/updated
20/// afterward.
21#[derive(Debug, Clone)]
22pub struct EthBuiltPayload {
23    /// Identifier of the payload
24    pub(crate) id: PayloadId,
25    /// The built block
26    pub(crate) block: Arc<SealedBlock<Block>>,
27    /// The fees of the block
28    pub(crate) fees: U256,
29    /// The blobs, proofs, and commitments in the block. If the block is pre-cancun, this will be
30    /// empty.
31    pub(crate) sidecars: Vec<BlobTransactionSidecar>,
32    /// The requests of the payload
33    pub(crate) requests: Option<Requests>,
34}
35
36// === impl BuiltPayload ===
37
38impl EthBuiltPayload {
39    /// Initializes the payload with the given initial block
40    ///
41    /// Caution: This does not set any [`BlobTransactionSidecar`].
42    pub const fn new(
43        id: PayloadId,
44        block: Arc<SealedBlock<Block>>,
45        fees: U256,
46        requests: Option<Requests>,
47    ) -> Self {
48        Self { id, block, fees, sidecars: Vec::new(), requests }
49    }
50
51    /// Returns the identifier of the payload.
52    pub const fn id(&self) -> PayloadId {
53        self.id
54    }
55
56    /// Returns the built block(sealed)
57    pub fn block(&self) -> &SealedBlock<Block> {
58        &self.block
59    }
60
61    /// Fees of the block
62    pub const fn fees(&self) -> U256 {
63        self.fees
64    }
65
66    /// Returns the blob sidecars.
67    pub fn sidecars(&self) -> &[BlobTransactionSidecar] {
68        &self.sidecars
69    }
70
71    /// Adds sidecars to the payload.
72    pub fn extend_sidecars(&mut self, sidecars: impl IntoIterator<Item = BlobTransactionSidecar>) {
73        self.sidecars.extend(sidecars)
74    }
75
76    /// Same as [`Self::extend_sidecars`] but returns the type again.
77    pub fn with_sidecars(
78        mut self,
79        sidecars: impl IntoIterator<Item = BlobTransactionSidecar>,
80    ) -> Self {
81        self.extend_sidecars(sidecars);
82        self
83    }
84}
85
86impl BuiltPayload for EthBuiltPayload {
87    type Primitives = EthPrimitives;
88
89    fn block(&self) -> &SealedBlock<Block> {
90        &self.block
91    }
92
93    fn fees(&self) -> U256 {
94        self.fees
95    }
96
97    fn requests(&self) -> Option<Requests> {
98        self.requests.clone()
99    }
100}
101
102// V1 engine_getPayloadV1 response
103impl From<EthBuiltPayload> for ExecutionPayloadV1 {
104    fn from(value: EthBuiltPayload) -> Self {
105        Self::from_block_unchecked(
106            value.block().hash(),
107            &Arc::unwrap_or_clone(value.block).into_block(),
108        )
109    }
110}
111
112// V2 engine_getPayloadV2 response
113impl From<EthBuiltPayload> for ExecutionPayloadEnvelopeV2 {
114    fn from(value: EthBuiltPayload) -> Self {
115        let EthBuiltPayload { block, fees, .. } = value;
116
117        Self {
118            block_value: fees,
119            execution_payload: ExecutionPayloadFieldV2::from_block_unchecked(
120                block.hash(),
121                &Arc::unwrap_or_clone(block).into_block(),
122            ),
123        }
124    }
125}
126
127impl From<EthBuiltPayload> for ExecutionPayloadEnvelopeV3 {
128    fn from(value: EthBuiltPayload) -> Self {
129        let EthBuiltPayload { block, fees, sidecars, .. } = value;
130
131        Self {
132            execution_payload: ExecutionPayloadV3::from_block_unchecked(
133                block.hash(),
134                &Arc::unwrap_or_clone(block).into_block(),
135            ),
136            block_value: fees,
137            // From the engine API spec:
138            //
139            // > Client software **MAY** use any heuristics to decide whether to set
140            // `shouldOverrideBuilder` flag or not. If client software does not implement any
141            // heuristic this flag **SHOULD** be set to `false`.
142            //
143            // Spec:
144            // <https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/cancun.md#specification-2>
145            should_override_builder: false,
146            blobs_bundle: sidecars.into(),
147        }
148    }
149}
150
151impl From<EthBuiltPayload> for ExecutionPayloadEnvelopeV4 {
152    fn from(value: EthBuiltPayload) -> Self {
153        Self {
154            execution_requests: value.requests.clone().unwrap_or_default(),
155            envelope_inner: value.into(),
156        }
157    }
158}
159
160/// Container type for all components required to build a payload.
161#[derive(Debug, Clone, PartialEq, Eq, Default)]
162pub struct EthPayloadBuilderAttributes {
163    /// Id of the payload
164    pub id: PayloadId,
165    /// Parent block to build the payload on top
166    pub parent: B256,
167    /// Unix timestamp for the generated payload
168    ///
169    /// Number of seconds since the Unix epoch.
170    pub timestamp: u64,
171    /// Address of the recipient for collecting transaction fee
172    pub suggested_fee_recipient: Address,
173    /// Randomness value for the generated payload
174    pub prev_randao: B256,
175    /// Withdrawals for the generated payload
176    pub withdrawals: Withdrawals,
177    /// Root of the parent beacon block
178    pub parent_beacon_block_root: Option<B256>,
179}
180
181// === impl EthPayloadBuilderAttributes ===
182
183impl EthPayloadBuilderAttributes {
184    /// Returns the identifier of the payload.
185    pub const fn payload_id(&self) -> PayloadId {
186        self.id
187    }
188
189    /// Creates a new payload builder for the given parent block and the attributes.
190    ///
191    /// Derives the unique [`PayloadId`] for the given parent and attributes
192    pub fn new(parent: B256, attributes: PayloadAttributes) -> Self {
193        let id = payload_id(&parent, &attributes);
194
195        Self {
196            id,
197            parent,
198            timestamp: attributes.timestamp,
199            suggested_fee_recipient: attributes.suggested_fee_recipient,
200            prev_randao: attributes.prev_randao,
201            withdrawals: attributes.withdrawals.unwrap_or_default().into(),
202            parent_beacon_block_root: attributes.parent_beacon_block_root,
203        }
204    }
205}
206
207impl PayloadBuilderAttributes for EthPayloadBuilderAttributes {
208    type RpcPayloadAttributes = PayloadAttributes;
209    type Error = Infallible;
210
211    /// Creates a new payload builder for the given parent block and the attributes.
212    ///
213    /// Derives the unique [`PayloadId`] for the given parent and attributes
214    fn try_new(
215        parent: B256,
216        attributes: PayloadAttributes,
217        _version: u8,
218    ) -> Result<Self, Infallible> {
219        Ok(Self::new(parent, attributes))
220    }
221
222    fn payload_id(&self) -> PayloadId {
223        self.id
224    }
225
226    fn parent(&self) -> B256 {
227        self.parent
228    }
229
230    fn timestamp(&self) -> u64 {
231        self.timestamp
232    }
233
234    fn parent_beacon_block_root(&self) -> Option<B256> {
235        self.parent_beacon_block_root
236    }
237
238    fn suggested_fee_recipient(&self) -> Address {
239        self.suggested_fee_recipient
240    }
241
242    fn prev_randao(&self) -> B256 {
243        self.prev_randao
244    }
245
246    fn withdrawals(&self) -> &Withdrawals {
247        &self.withdrawals
248    }
249}
250
251/// Generates the payload id for the configured payload from the [`PayloadAttributes`].
252///
253/// Returns an 8-byte identifier by hashing the payload components with sha256 hash.
254pub(crate) fn payload_id(parent: &B256, attributes: &PayloadAttributes) -> PayloadId {
255    use sha2::Digest;
256    let mut hasher = sha2::Sha256::new();
257    hasher.update(parent.as_slice());
258    hasher.update(&attributes.timestamp.to_be_bytes()[..]);
259    hasher.update(attributes.prev_randao.as_slice());
260    hasher.update(attributes.suggested_fee_recipient.as_slice());
261    if let Some(withdrawals) = &attributes.withdrawals {
262        let mut buf = Vec::new();
263        withdrawals.encode(&mut buf);
264        hasher.update(buf);
265    }
266
267    if let Some(parent_beacon_block) = attributes.parent_beacon_block_root {
268        hasher.update(parent_beacon_block);
269    }
270
271    let out = hasher.finalize();
272    PayloadId::new(out.as_slice()[..8].try_into().expect("sufficient length"))
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278    use alloy_eips::eip4895::Withdrawal;
279    use alloy_primitives::B64;
280    use core::str::FromStr;
281
282    #[test]
283    fn attributes_serde() {
284        let attributes = r#"{"timestamp":"0x1235","prevRandao":"0xf343b00e02dc34ec0124241f74f32191be28fb370bb48060f5fa4df99bda774c","suggestedFeeRecipient":"0x0000000000000000000000000000000000000000","withdrawals":null,"parentBeaconBlockRoot":null}"#;
285        let _attributes: PayloadAttributes = serde_json::from_str(attributes).unwrap();
286    }
287
288    #[test]
289    fn test_payload_id_basic() {
290        // Create a parent block and payload attributes
291        let parent =
292            B256::from_str("0x3b8fb240d288781d4aac94d3fd16809ee413bc99294a085798a589dae51ddd4a")
293                .unwrap();
294        let attributes = PayloadAttributes {
295            timestamp: 0x5,
296            prev_randao: B256::from_str(
297                "0x0000000000000000000000000000000000000000000000000000000000000000",
298            )
299            .unwrap(),
300            suggested_fee_recipient: Address::from_str(
301                "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b",
302            )
303            .unwrap(),
304            withdrawals: None,
305            parent_beacon_block_root: None,
306        };
307
308        // Verify that the generated payload ID matches the expected value
309        assert_eq!(
310            payload_id(&parent, &attributes),
311            PayloadId(B64::from_str("0xa247243752eb10b4").unwrap())
312        );
313    }
314
315    #[test]
316    fn test_payload_id_with_withdrawals() {
317        // Set up the parent and attributes with withdrawals
318        let parent =
319            B256::from_str("0x9876543210abcdef9876543210abcdef9876543210abcdef9876543210abcdef")
320                .unwrap();
321        let attributes = PayloadAttributes {
322            timestamp: 1622553200,
323            prev_randao: B256::from_slice(&[1; 32]),
324            suggested_fee_recipient: Address::from_str(
325                "0xb94f5374fce5edbc8e2a8697c15331677e6ebf0b",
326            )
327            .unwrap(),
328            withdrawals: Some(vec![
329                Withdrawal {
330                    index: 1,
331                    validator_index: 123,
332                    address: Address::from([0xAA; 20]),
333                    amount: 10,
334                },
335                Withdrawal {
336                    index: 2,
337                    validator_index: 456,
338                    address: Address::from([0xBB; 20]),
339                    amount: 20,
340                },
341            ]),
342            parent_beacon_block_root: None,
343        };
344
345        // Verify that the generated payload ID matches the expected value
346        assert_eq!(
347            payload_id(&parent, &attributes),
348            PayloadId(B64::from_str("0xedddc2f84ba59865").unwrap())
349        );
350    }
351
352    #[test]
353    fn test_payload_id_with_parent_beacon_block_root() {
354        // Set up the parent and attributes with a parent beacon block root
355        let parent =
356            B256::from_str("0x9876543210abcdef9876543210abcdef9876543210abcdef9876543210abcdef")
357                .unwrap();
358        let attributes = PayloadAttributes {
359            timestamp: 1622553200,
360            prev_randao: B256::from_str(
361                "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234",
362            )
363            .unwrap(),
364            suggested_fee_recipient: Address::from_str(
365                "0xc94f5374fce5edbc8e2a8697c15331677e6ebf0b",
366            )
367            .unwrap(),
368            withdrawals: None,
369            parent_beacon_block_root: Some(
370                B256::from_str(
371                    "0x2222222222222222222222222222222222222222222222222222222222222222",
372                )
373                .unwrap(),
374            ),
375        };
376
377        // Verify that the generated payload ID matches the expected value
378        assert_eq!(
379            payload_id(&parent, &attributes),
380            PayloadId(B64::from_str("0x0fc49cd532094cce").unwrap())
381        );
382    }
383}