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