reth_optimism_payload_builder/
payload.rs

1//! Payload related types
2
3use std::{fmt::Debug, sync::Arc};
4
5use alloy_consensus::{Block, BlockHeader};
6use alloy_eips::{
7    eip1559::BaseFeeParams, eip2718::Decodable2718, eip4895::Withdrawals, eip7685::Requests,
8};
9use alloy_primitives::{keccak256, Address, Bytes, B256, B64, U256};
10use alloy_rlp::Encodable;
11use alloy_rpc_types_engine::{
12    BlobsBundleV1, ExecutionPayloadEnvelopeV2, ExecutionPayloadFieldV2, ExecutionPayloadV1,
13    ExecutionPayloadV3, PayloadId,
14};
15use op_alloy_consensus::{encode_holocene_extra_data, encode_jovian_extra_data, EIP1559ParamError};
16use op_alloy_rpc_types_engine::{
17    OpExecutionPayloadEnvelopeV3, OpExecutionPayloadEnvelopeV4, OpExecutionPayloadV4,
18};
19use reth_chain_state::ExecutedBlockWithTrieUpdates;
20use reth_chainspec::EthChainSpec;
21use reth_optimism_evm::OpNextBlockEnvAttributes;
22use reth_optimism_forks::OpHardforks;
23use reth_payload_builder::{EthPayloadBuilderAttributes, PayloadBuilderError};
24use reth_payload_primitives::{BuildNextEnv, BuiltPayload, PayloadBuilderAttributes};
25use reth_primitives_traits::{
26    NodePrimitives, SealedBlock, SealedHeader, SignedTransaction, WithEncoded,
27};
28
29/// Re-export for use in downstream arguments.
30pub use op_alloy_rpc_types_engine::OpPayloadAttributes;
31use reth_optimism_primitives::OpPrimitives;
32
33/// Optimism Payload Builder Attributes
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct OpPayloadBuilderAttributes<T> {
36    /// Inner ethereum payload builder attributes
37    pub payload_attributes: EthPayloadBuilderAttributes,
38    /// `NoTxPool` option for the generated payload
39    pub no_tx_pool: bool,
40    /// Decoded transactions and the original EIP-2718 encoded bytes as received in the payload
41    /// attributes.
42    pub transactions: Vec<WithEncoded<T>>,
43    /// The gas limit for the generated payload
44    pub gas_limit: Option<u64>,
45    /// EIP-1559 parameters for the generated payload
46    pub eip_1559_params: Option<B64>,
47    /// Min base fee for the generated payload (only available post-Jovian)
48    pub min_base_fee: Option<u64>,
49}
50
51impl<T> Default for OpPayloadBuilderAttributes<T> {
52    fn default() -> Self {
53        Self {
54            payload_attributes: Default::default(),
55            no_tx_pool: Default::default(),
56            gas_limit: Default::default(),
57            eip_1559_params: Default::default(),
58            transactions: Default::default(),
59            min_base_fee: Default::default(),
60        }
61    }
62}
63
64impl<T> OpPayloadBuilderAttributes<T> {
65    /// Extracts the extra data parameters post-Holocene hardfork.
66    /// In Holocene, those parameters are the EIP-1559 base fee parameters.
67    pub fn get_holocene_extra_data(
68        &self,
69        default_base_fee_params: BaseFeeParams,
70    ) -> Result<Bytes, EIP1559ParamError> {
71        self.eip_1559_params
72            .map(|params| encode_holocene_extra_data(params, default_base_fee_params))
73            .ok_or(EIP1559ParamError::NoEIP1559Params)?
74    }
75
76    /// Extracts the extra data parameters post-Jovian hardfork.
77    /// Those parameters are the EIP-1559 parameters from Holocene and the minimum base fee.
78    pub fn get_jovian_extra_data(
79        &self,
80        default_base_fee_params: BaseFeeParams,
81    ) -> Result<Bytes, EIP1559ParamError> {
82        let min_base_fee = self.min_base_fee.ok_or(EIP1559ParamError::MinBaseFeeNotSet)?;
83        self.eip_1559_params
84            .map(|params| encode_jovian_extra_data(params, default_base_fee_params, min_base_fee))
85            .ok_or(EIP1559ParamError::NoEIP1559Params)?
86    }
87}
88
89impl<T: Decodable2718 + Send + Sync + Debug + Unpin + 'static> PayloadBuilderAttributes
90    for OpPayloadBuilderAttributes<T>
91{
92    type RpcPayloadAttributes = OpPayloadAttributes;
93    type Error = alloy_rlp::Error;
94
95    /// Creates a new payload builder for the given parent block and the attributes.
96    ///
97    /// Derives the unique [`PayloadId`] for the given parent and attributes
98    fn try_new(
99        parent: B256,
100        attributes: OpPayloadAttributes,
101        version: u8,
102    ) -> Result<Self, Self::Error> {
103        let id = payload_id_optimism(&parent, &attributes, version);
104
105        let transactions = attributes
106            .transactions
107            .unwrap_or_default()
108            .into_iter()
109            .map(|data| {
110                Decodable2718::decode_2718_exact(data.as_ref()).map(|tx| WithEncoded::new(data, tx))
111            })
112            .collect::<Result<_, _>>()?;
113
114        let payload_attributes = EthPayloadBuilderAttributes {
115            id,
116            parent,
117            timestamp: attributes.payload_attributes.timestamp,
118            suggested_fee_recipient: attributes.payload_attributes.suggested_fee_recipient,
119            prev_randao: attributes.payload_attributes.prev_randao,
120            withdrawals: attributes.payload_attributes.withdrawals.unwrap_or_default().into(),
121            parent_beacon_block_root: attributes.payload_attributes.parent_beacon_block_root,
122        };
123
124        Ok(Self {
125            payload_attributes,
126            no_tx_pool: attributes.no_tx_pool.unwrap_or_default(),
127            transactions,
128            gas_limit: attributes.gas_limit,
129            eip_1559_params: attributes.eip_1559_params,
130            min_base_fee: attributes.min_base_fee,
131        })
132    }
133
134    fn payload_id(&self) -> PayloadId {
135        self.payload_attributes.id
136    }
137
138    fn parent(&self) -> B256 {
139        self.payload_attributes.parent
140    }
141
142    fn timestamp(&self) -> u64 {
143        self.payload_attributes.timestamp
144    }
145
146    fn parent_beacon_block_root(&self) -> Option<B256> {
147        self.payload_attributes.parent_beacon_block_root
148    }
149
150    fn suggested_fee_recipient(&self) -> Address {
151        self.payload_attributes.suggested_fee_recipient
152    }
153
154    fn prev_randao(&self) -> B256 {
155        self.payload_attributes.prev_randao
156    }
157
158    fn withdrawals(&self) -> &Withdrawals {
159        &self.payload_attributes.withdrawals
160    }
161}
162
163impl<OpTransactionSigned> From<EthPayloadBuilderAttributes>
164    for OpPayloadBuilderAttributes<OpTransactionSigned>
165{
166    fn from(value: EthPayloadBuilderAttributes) -> Self {
167        Self { payload_attributes: value, ..Default::default() }
168    }
169}
170
171/// Contains the built payload.
172#[derive(Debug, Clone)]
173pub struct OpBuiltPayload<N: NodePrimitives = OpPrimitives> {
174    /// Identifier of the payload
175    pub(crate) id: PayloadId,
176    /// Sealed block
177    pub(crate) block: Arc<SealedBlock<N::Block>>,
178    /// Block execution data for the payload, if any.
179    pub(crate) executed_block: Option<ExecutedBlockWithTrieUpdates<N>>,
180    /// The fees of the block
181    pub(crate) fees: U256,
182}
183
184// === impl BuiltPayload ===
185
186impl<N: NodePrimitives> OpBuiltPayload<N> {
187    /// Initializes the payload with the given initial block.
188    pub const fn new(
189        id: PayloadId,
190        block: Arc<SealedBlock<N::Block>>,
191        fees: U256,
192        executed_block: Option<ExecutedBlockWithTrieUpdates<N>>,
193    ) -> Self {
194        Self { id, block, fees, executed_block }
195    }
196
197    /// Returns the identifier of the payload.
198    pub const fn id(&self) -> PayloadId {
199        self.id
200    }
201
202    /// Returns the built block(sealed)
203    pub fn block(&self) -> &SealedBlock<N::Block> {
204        &self.block
205    }
206
207    /// Fees of the block
208    pub const fn fees(&self) -> U256 {
209        self.fees
210    }
211
212    /// Converts the value into [`SealedBlock`].
213    pub fn into_sealed_block(self) -> SealedBlock<N::Block> {
214        Arc::unwrap_or_clone(self.block)
215    }
216}
217
218impl<N: NodePrimitives> BuiltPayload for OpBuiltPayload<N> {
219    type Primitives = N;
220
221    fn block(&self) -> &SealedBlock<N::Block> {
222        self.block()
223    }
224
225    fn fees(&self) -> U256 {
226        self.fees
227    }
228
229    fn executed_block(&self) -> Option<ExecutedBlockWithTrieUpdates<N>> {
230        self.executed_block.clone()
231    }
232
233    fn requests(&self) -> Option<Requests> {
234        None
235    }
236}
237
238// V1 engine_getPayloadV1 response
239impl<T, N> From<OpBuiltPayload<N>> for ExecutionPayloadV1
240where
241    T: SignedTransaction,
242    N: NodePrimitives<Block = Block<T>>,
243{
244    fn from(value: OpBuiltPayload<N>) -> Self {
245        Self::from_block_unchecked(
246            value.block().hash(),
247            &Arc::unwrap_or_clone(value.block).into_block(),
248        )
249    }
250}
251
252// V2 engine_getPayloadV2 response
253impl<T, N> From<OpBuiltPayload<N>> for ExecutionPayloadEnvelopeV2
254where
255    T: SignedTransaction,
256    N: NodePrimitives<Block = Block<T>>,
257{
258    fn from(value: OpBuiltPayload<N>) -> Self {
259        let OpBuiltPayload { block, fees, .. } = value;
260
261        Self {
262            block_value: fees,
263            execution_payload: ExecutionPayloadFieldV2::from_block_unchecked(
264                block.hash(),
265                &Arc::unwrap_or_clone(block).into_block(),
266            ),
267        }
268    }
269}
270
271impl<T, N> From<OpBuiltPayload<N>> for OpExecutionPayloadEnvelopeV3
272where
273    T: SignedTransaction,
274    N: NodePrimitives<Block = Block<T>>,
275{
276    fn from(value: OpBuiltPayload<N>) -> Self {
277        let OpBuiltPayload { block, fees, .. } = value;
278
279        let parent_beacon_block_root = block.parent_beacon_block_root.unwrap_or_default();
280
281        Self {
282            execution_payload: ExecutionPayloadV3::from_block_unchecked(
283                block.hash(),
284                &Arc::unwrap_or_clone(block).into_block(),
285            ),
286            block_value: fees,
287            // From the engine API spec:
288            //
289            // > Client software **MAY** use any heuristics to decide whether to set
290            // `shouldOverrideBuilder` flag or not. If client software does not implement any
291            // heuristic this flag **SHOULD** be set to `false`.
292            //
293            // Spec:
294            // <https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/cancun.md#specification-2>
295            should_override_builder: false,
296            // No blobs for OP.
297            blobs_bundle: BlobsBundleV1 { blobs: vec![], commitments: vec![], proofs: vec![] },
298            parent_beacon_block_root,
299        }
300    }
301}
302
303impl<T, N> From<OpBuiltPayload<N>> for OpExecutionPayloadEnvelopeV4
304where
305    T: SignedTransaction,
306    N: NodePrimitives<Block = Block<T>>,
307{
308    fn from(value: OpBuiltPayload<N>) -> Self {
309        let OpBuiltPayload { block, fees, .. } = value;
310
311        let parent_beacon_block_root = block.parent_beacon_block_root.unwrap_or_default();
312
313        let l2_withdrawals_root = block.withdrawals_root.unwrap_or_default();
314        let payload_v3 = ExecutionPayloadV3::from_block_unchecked(
315            block.hash(),
316            &Arc::unwrap_or_clone(block).into_block(),
317        );
318
319        Self {
320            execution_payload: OpExecutionPayloadV4::from_v3_with_withdrawals_root(
321                payload_v3,
322                l2_withdrawals_root,
323            ),
324            block_value: fees,
325            // From the engine API spec:
326            //
327            // > Client software **MAY** use any heuristics to decide whether to set
328            // `shouldOverrideBuilder` flag or not. If client software does not implement any
329            // heuristic this flag **SHOULD** be set to `false`.
330            //
331            // Spec:
332            // <https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/cancun.md#specification-2>
333            should_override_builder: false,
334            // No blobs for OP.
335            blobs_bundle: BlobsBundleV1 { blobs: vec![], commitments: vec![], proofs: vec![] },
336            parent_beacon_block_root,
337            execution_requests: vec![],
338        }
339    }
340}
341
342/// Generates the payload id for the configured payload from the [`OpPayloadAttributes`].
343///
344/// Returns an 8-byte identifier by hashing the payload components with sha256 hash.
345pub fn payload_id_optimism(
346    parent: &B256,
347    attributes: &OpPayloadAttributes,
348    payload_version: u8,
349) -> PayloadId {
350    use sha2::Digest;
351    let mut hasher = sha2::Sha256::new();
352    hasher.update(parent.as_slice());
353    hasher.update(&attributes.payload_attributes.timestamp.to_be_bytes()[..]);
354    hasher.update(attributes.payload_attributes.prev_randao.as_slice());
355    hasher.update(attributes.payload_attributes.suggested_fee_recipient.as_slice());
356    if let Some(withdrawals) = &attributes.payload_attributes.withdrawals {
357        let mut buf = Vec::new();
358        withdrawals.encode(&mut buf);
359        hasher.update(buf);
360    }
361
362    if let Some(parent_beacon_block) = attributes.payload_attributes.parent_beacon_block_root {
363        hasher.update(parent_beacon_block);
364    }
365
366    let no_tx_pool = attributes.no_tx_pool.unwrap_or_default();
367    if no_tx_pool || attributes.transactions.as_ref().is_some_and(|txs| !txs.is_empty()) {
368        hasher.update([no_tx_pool as u8]);
369        let txs_len = attributes.transactions.as_ref().map(|txs| txs.len()).unwrap_or_default();
370        hasher.update(&txs_len.to_be_bytes()[..]);
371        if let Some(txs) = &attributes.transactions {
372            for tx in txs {
373                // we have to just hash the bytes here because otherwise we would need to decode
374                // the transactions here which really isn't ideal
375                let tx_hash = keccak256(tx);
376                // maybe we can try just taking the hash and not decoding
377                hasher.update(tx_hash)
378            }
379        }
380    }
381
382    if let Some(gas_limit) = attributes.gas_limit {
383        hasher.update(gas_limit.to_be_bytes());
384    }
385
386    if let Some(eip_1559_params) = attributes.eip_1559_params {
387        hasher.update(eip_1559_params.as_slice());
388    }
389
390    let mut out = hasher.finalize();
391    out[0] = payload_version;
392    PayloadId::new(out.as_slice()[..8].try_into().expect("sufficient length"))
393}
394
395impl<H, T, ChainSpec> BuildNextEnv<OpPayloadBuilderAttributes<T>, H, ChainSpec>
396    for OpNextBlockEnvAttributes
397where
398    H: BlockHeader,
399    T: SignedTransaction,
400    ChainSpec: EthChainSpec + OpHardforks,
401{
402    fn build_next_env(
403        attributes: &OpPayloadBuilderAttributes<T>,
404        parent: &SealedHeader<H>,
405        chain_spec: &ChainSpec,
406    ) -> Result<Self, PayloadBuilderError> {
407        let extra_data = if chain_spec.is_jovian_active_at_timestamp(attributes.timestamp()) {
408            attributes
409                .get_jovian_extra_data(
410                    chain_spec.base_fee_params_at_timestamp(attributes.timestamp()),
411                )
412                .map_err(PayloadBuilderError::other)?
413        } else if chain_spec.is_holocene_active_at_timestamp(attributes.timestamp()) {
414            attributes
415                .get_holocene_extra_data(
416                    chain_spec.base_fee_params_at_timestamp(attributes.timestamp()),
417                )
418                .map_err(PayloadBuilderError::other)?
419        } else {
420            Default::default()
421        };
422
423        Ok(Self {
424            timestamp: attributes.timestamp(),
425            suggested_fee_recipient: attributes.suggested_fee_recipient(),
426            prev_randao: attributes.prev_randao(),
427            gas_limit: attributes.gas_limit.unwrap_or_else(|| parent.gas_limit()),
428            parent_beacon_block_root: attributes.parent_beacon_block_root(),
429            extra_data,
430        })
431    }
432}
433
434#[cfg(test)]
435mod tests {
436    use super::*;
437    use crate::OpPayloadAttributes;
438    use alloy_primitives::{address, b256, bytes, FixedBytes};
439    use alloy_rpc_types_engine::PayloadAttributes;
440    use reth_optimism_primitives::OpTransactionSigned;
441    use reth_payload_primitives::EngineApiMessageVersion;
442    use std::str::FromStr;
443
444    #[test]
445    fn test_payload_id_parity_op_geth() {
446        // INFO rollup_boost::server:received fork_choice_updated_v3 from builder and l2_client
447        // payload_id_builder="0x6ef26ca02318dcf9" payload_id_l2="0x03d2dae446d2a86a"
448        let expected =
449            PayloadId::new(FixedBytes::<8>::from_str("0x03d2dae446d2a86a").unwrap().into());
450        let attrs = OpPayloadAttributes {
451            payload_attributes: PayloadAttributes {
452                timestamp: 1728933301,
453                prev_randao: b256!("0x9158595abbdab2c90635087619aa7042bbebe47642dfab3c9bfb934f6b082765"),
454                suggested_fee_recipient: address!("0x4200000000000000000000000000000000000011"),
455                withdrawals: Some([].into()),
456                parent_beacon_block_root: b256!("0x8fe0193b9bf83cb7e5a08538e494fecc23046aab9a497af3704f4afdae3250ff").into(),
457            },
458            transactions: Some([bytes!("7ef8f8a0dc19cfa777d90980e4875d0a548a881baaa3f83f14d1bc0d3038bc329350e54194deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e20000f424000000000000000000000000300000000670d6d890000000000000125000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000014bf9181db6e381d4384bbf69c48b0ee0eed23c6ca26143c6d2544f9d39997a590000000000000000000000007f83d659683caf2767fd3c720981d51f5bc365bc")].into()),
459            no_tx_pool: None,
460            gas_limit: Some(30000000),
461            eip_1559_params: None,
462            min_base_fee: None,
463        };
464
465        // Reth's `PayloadId` should match op-geth's `PayloadId`. This fails
466        assert_eq!(
467            expected,
468            payload_id_optimism(
469                &b256!("0x3533bf30edaf9505d0810bf475cbe4e5f4b9889904b9845e83efdeab4e92eb1e"),
470                &attrs,
471                EngineApiMessageVersion::V3 as u8
472            )
473        );
474    }
475
476    #[test]
477    fn test_get_extra_data_post_holocene() {
478        let attributes: OpPayloadBuilderAttributes<OpTransactionSigned> =
479            OpPayloadBuilderAttributes {
480                eip_1559_params: Some(B64::from_str("0x0000000800000008").unwrap()),
481                ..Default::default()
482            };
483        let extra_data = attributes.get_holocene_extra_data(BaseFeeParams::new(80, 60));
484        assert_eq!(extra_data.unwrap(), Bytes::copy_from_slice(&[0, 0, 0, 0, 8, 0, 0, 0, 8]));
485    }
486
487    #[test]
488    fn test_get_extra_data_post_holocene_default() {
489        let attributes: OpPayloadBuilderAttributes<OpTransactionSigned> =
490            OpPayloadBuilderAttributes { eip_1559_params: Some(B64::ZERO), ..Default::default() };
491        let extra_data = attributes.get_holocene_extra_data(BaseFeeParams::new(80, 60));
492        assert_eq!(extra_data.unwrap(), Bytes::copy_from_slice(&[0, 0, 0, 0, 80, 0, 0, 0, 60]));
493    }
494
495    #[test]
496    fn test_get_extra_data_post_jovian() {
497        let attributes: OpPayloadBuilderAttributes<OpTransactionSigned> =
498            OpPayloadBuilderAttributes {
499                eip_1559_params: Some(B64::from_str("0x0000000800000008").unwrap()),
500                min_base_fee: Some(10),
501                ..Default::default()
502            };
503        let extra_data = attributes.get_jovian_extra_data(BaseFeeParams::new(80, 60));
504        assert_eq!(
505            extra_data.unwrap(),
506            // Version byte is 1 for Jovian, then holocene payload followed by 8 bytes for the
507            // minimum base fee
508            Bytes::copy_from_slice(&[1, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 10])
509        );
510    }
511
512    #[test]
513    fn test_get_extra_data_post_jovian_default() {
514        let attributes: OpPayloadBuilderAttributes<OpTransactionSigned> =
515            OpPayloadBuilderAttributes {
516                eip_1559_params: Some(B64::ZERO),
517                min_base_fee: Some(10),
518                ..Default::default()
519            };
520        let extra_data = attributes.get_jovian_extra_data(BaseFeeParams::new(80, 60));
521        assert_eq!(
522            extra_data.unwrap(),
523            // Version byte is 1 for Jovian, then holocene payload followed by 8 bytes for the
524            // minimum base fee
525            Bytes::copy_from_slice(&[1, 0, 0, 0, 80, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0, 0, 10])
526        );
527    }
528
529    #[test]
530    fn test_get_extra_data_post_jovian_no_base_fee() {
531        let attributes: OpPayloadBuilderAttributes<OpTransactionSigned> =
532            OpPayloadBuilderAttributes {
533                eip_1559_params: Some(B64::ZERO),
534                min_base_fee: None,
535                ..Default::default()
536            };
537        let extra_data = attributes.get_jovian_extra_data(BaseFeeParams::new(80, 60));
538        assert_eq!(extra_data.unwrap_err(), EIP1559ParamError::MinBaseFeeNotSet);
539    }
540}