reth_ethereum_engine_primitives/
payload.rs

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