reth_optimism_rpc/eth/
receipt.rs

1//! Loads and formats OP receipt RPC response.
2
3use crate::{eth::RpcNodeCore, OpEthApi, OpEthApiError};
4use alloy_consensus::{BlockHeader, Receipt, ReceiptWithBloom, TxReceipt};
5use alloy_eips::eip2718::Encodable2718;
6use alloy_rpc_types_eth::{Log, TransactionReceipt};
7use op_alloy_consensus::{OpReceipt, OpTransaction};
8use op_alloy_rpc_types::{L1BlockInfo, OpTransactionReceipt, OpTransactionReceiptFields};
9use op_revm::estimate_tx_compressed_size;
10use reth_chainspec::ChainSpecProvider;
11use reth_node_api::NodePrimitives;
12use reth_optimism_evm::RethL1BlockInfo;
13use reth_optimism_forks::OpHardforks;
14use reth_primitives_traits::SealedBlock;
15use reth_rpc_eth_api::{
16    helpers::LoadReceipt,
17    transaction::{ConvertReceiptInput, ReceiptConverter},
18    RpcConvert,
19};
20use reth_rpc_eth_types::{receipt::build_receipt, EthApiError};
21use reth_storage_api::BlockReader;
22use std::fmt::Debug;
23
24impl<N, Rpc> LoadReceipt for OpEthApi<N, Rpc>
25where
26    N: RpcNodeCore,
27    Rpc: RpcConvert<Primitives = N::Primitives, Error = OpEthApiError>,
28{
29}
30
31/// Converter for OP receipts.
32#[derive(Debug, Clone)]
33pub struct OpReceiptConverter<Provider> {
34    provider: Provider,
35}
36
37impl<Provider> OpReceiptConverter<Provider> {
38    /// Creates a new [`OpReceiptConverter`].
39    pub const fn new(provider: Provider) -> Self {
40        Self { provider }
41    }
42}
43
44impl<Provider, N> ReceiptConverter<N> for OpReceiptConverter<Provider>
45where
46    N: NodePrimitives<SignedTx: OpTransaction, Receipt = OpReceipt>,
47    Provider:
48        BlockReader<Block = N::Block> + ChainSpecProvider<ChainSpec: OpHardforks> + Debug + 'static,
49{
50    type RpcReceipt = OpTransactionReceipt;
51    type Error = OpEthApiError;
52
53    fn convert_receipts(
54        &self,
55        inputs: Vec<ConvertReceiptInput<'_, N>>,
56    ) -> Result<Vec<Self::RpcReceipt>, Self::Error> {
57        let Some(block_number) = inputs.first().map(|r| r.meta.block_number) else {
58            return Ok(Vec::new());
59        };
60
61        let block = self
62            .provider
63            .block_by_number(block_number)?
64            .ok_or(EthApiError::HeaderNotFound(block_number.into()))?;
65
66        self.convert_receipts_with_block(inputs, &SealedBlock::new_unhashed(block))
67    }
68
69    fn convert_receipts_with_block(
70        &self,
71        inputs: Vec<ConvertReceiptInput<'_, N>>,
72        block: &SealedBlock<N::Block>,
73    ) -> Result<Vec<Self::RpcReceipt>, Self::Error> {
74        let mut l1_block_info = match reth_optimism_evm::extract_l1_info(block.body()) {
75            Ok(l1_block_info) => l1_block_info,
76            Err(err) => {
77                // If it is the genesis block (i.e. block number is 0), there is no L1 info, so
78                // we return an empty l1_block_info.
79                if block.header().number() == 0 {
80                    return Ok(vec![]);
81                }
82                return Err(err.into());
83            }
84        };
85
86        let mut receipts = Vec::with_capacity(inputs.len());
87
88        for input in inputs {
89            // We must clear this cache as different L2 transactions can have different
90            // L1 costs. A potential improvement here is to only clear the cache if the
91            // new transaction input has changed, since otherwise the L1 cost wouldn't.
92            l1_block_info.clear_tx_l1_cost();
93
94            receipts.push(
95                OpReceiptBuilder::new(&self.provider.chain_spec(), input, &mut l1_block_info)?
96                    .build(),
97            );
98        }
99
100        Ok(receipts)
101    }
102}
103
104/// L1 fee and data gas for a non-deposit transaction, or deposit nonce and receipt version for a
105/// deposit transaction.
106#[derive(Debug, Clone)]
107pub struct OpReceiptFieldsBuilder {
108    /// Block number.
109    pub block_number: u64,
110    /// Block timestamp.
111    pub block_timestamp: u64,
112    /// The L1 fee for transaction.
113    pub l1_fee: Option<u128>,
114    /// L1 gas used by transaction.
115    pub l1_data_gas: Option<u128>,
116    /// L1 fee scalar.
117    pub l1_fee_scalar: Option<f64>,
118    /* ---------------------------------------- Bedrock ---------------------------------------- */
119    /// The base fee of the L1 origin block.
120    pub l1_base_fee: Option<u128>,
121    /* --------------------------------------- Regolith ---------------------------------------- */
122    /// Deposit nonce, if this is a deposit transaction.
123    pub deposit_nonce: Option<u64>,
124    /* ---------------------------------------- Canyon ----------------------------------------- */
125    /// Deposit receipt version, if this is a deposit transaction.
126    pub deposit_receipt_version: Option<u64>,
127    /* ---------------------------------------- Ecotone ---------------------------------------- */
128    /// The current L1 fee scalar.
129    pub l1_base_fee_scalar: Option<u128>,
130    /// The current L1 blob base fee.
131    pub l1_blob_base_fee: Option<u128>,
132    /// The current L1 blob base fee scalar.
133    pub l1_blob_base_fee_scalar: Option<u128>,
134    /* ---------------------------------------- Isthmus ---------------------------------------- */
135    /// The current operator fee scalar.
136    pub operator_fee_scalar: Option<u128>,
137    /// The current L1 blob base fee scalar.
138    pub operator_fee_constant: Option<u128>,
139    /* ---------------------------------------- Jovian ----------------------------------------- */
140    /// The current DA footprint gas scalar.
141    pub da_footprint_gas_scalar: Option<u16>,
142}
143
144impl OpReceiptFieldsBuilder {
145    /// Returns a new builder.
146    pub const fn new(block_timestamp: u64, block_number: u64) -> Self {
147        Self {
148            block_number,
149            block_timestamp,
150            l1_fee: None,
151            l1_data_gas: None,
152            l1_fee_scalar: None,
153            l1_base_fee: None,
154            deposit_nonce: None,
155            deposit_receipt_version: None,
156            l1_base_fee_scalar: None,
157            l1_blob_base_fee: None,
158            l1_blob_base_fee_scalar: None,
159            operator_fee_scalar: None,
160            operator_fee_constant: None,
161            da_footprint_gas_scalar: None,
162        }
163    }
164
165    /// Applies [`L1BlockInfo`](op_revm::L1BlockInfo).
166    pub fn l1_block_info<T: Encodable2718 + OpTransaction>(
167        mut self,
168        chain_spec: &impl OpHardforks,
169        tx: &T,
170        l1_block_info: &mut op_revm::L1BlockInfo,
171    ) -> Result<Self, OpEthApiError> {
172        let raw_tx = tx.encoded_2718();
173        let timestamp = self.block_timestamp;
174
175        self.l1_fee = Some(
176            l1_block_info
177                .l1_tx_data_fee(chain_spec, timestamp, &raw_tx, tx.is_deposit())
178                .map_err(|_| OpEthApiError::L1BlockFeeError)?
179                .saturating_to(),
180        );
181
182        self.l1_data_gas = Some(
183            l1_block_info
184                .l1_data_gas(chain_spec, timestamp, &raw_tx)
185                .map_err(|_| OpEthApiError::L1BlockGasError)?
186                .saturating_add(l1_block_info.l1_fee_overhead.unwrap_or_default())
187                .saturating_to(),
188        );
189
190        self.l1_fee_scalar = (!chain_spec.is_ecotone_active_at_timestamp(timestamp))
191            .then_some(f64::from(l1_block_info.l1_base_fee_scalar) / 1_000_000.0);
192
193        self.l1_base_fee = Some(l1_block_info.l1_base_fee.saturating_to());
194        self.l1_base_fee_scalar = Some(l1_block_info.l1_base_fee_scalar.saturating_to());
195        self.l1_blob_base_fee = l1_block_info.l1_blob_base_fee.map(|fee| fee.saturating_to());
196        self.l1_blob_base_fee_scalar =
197            l1_block_info.l1_blob_base_fee_scalar.map(|scalar| scalar.saturating_to());
198
199        // If the operator fee params are both set to 0, we don't add them to the receipt.
200        let operator_fee_scalar_has_non_zero_value: bool =
201            l1_block_info.operator_fee_scalar.is_some_and(|scalar| !scalar.is_zero());
202
203        let operator_fee_constant_has_non_zero_value =
204            l1_block_info.operator_fee_constant.is_some_and(|constant| !constant.is_zero());
205
206        if operator_fee_scalar_has_non_zero_value || operator_fee_constant_has_non_zero_value {
207            self.operator_fee_scalar =
208                l1_block_info.operator_fee_scalar.map(|scalar| scalar.saturating_to());
209            self.operator_fee_constant =
210                l1_block_info.operator_fee_constant.map(|constant| constant.saturating_to());
211        }
212
213        self.da_footprint_gas_scalar = l1_block_info.da_footprint_gas_scalar;
214
215        Ok(self)
216    }
217
218    /// Applies deposit transaction metadata: deposit nonce.
219    pub const fn deposit_nonce(mut self, nonce: Option<u64>) -> Self {
220        self.deposit_nonce = nonce;
221        self
222    }
223
224    /// Applies deposit transaction metadata: deposit receipt version.
225    pub const fn deposit_version(mut self, version: Option<u64>) -> Self {
226        self.deposit_receipt_version = version;
227        self
228    }
229
230    /// Builds the [`OpTransactionReceiptFields`] object.
231    pub const fn build(self) -> OpTransactionReceiptFields {
232        let Self {
233            block_number: _,    // used to compute other fields
234            block_timestamp: _, // used to compute other fields
235            l1_fee,
236            l1_data_gas: l1_gas_used,
237            l1_fee_scalar,
238            l1_base_fee: l1_gas_price,
239            deposit_nonce,
240            deposit_receipt_version,
241            l1_base_fee_scalar,
242            l1_blob_base_fee,
243            l1_blob_base_fee_scalar,
244            operator_fee_scalar,
245            operator_fee_constant,
246            da_footprint_gas_scalar,
247        } = self;
248
249        OpTransactionReceiptFields {
250            l1_block_info: L1BlockInfo {
251                l1_gas_price,
252                l1_gas_used,
253                l1_fee,
254                l1_fee_scalar,
255                l1_base_fee_scalar,
256                l1_blob_base_fee,
257                l1_blob_base_fee_scalar,
258                operator_fee_scalar,
259                operator_fee_constant,
260                da_footprint_gas_scalar,
261            },
262            deposit_nonce,
263            deposit_receipt_version,
264        }
265    }
266}
267
268/// Builds an [`OpTransactionReceipt`].
269#[derive(Debug)]
270pub struct OpReceiptBuilder {
271    /// Core receipt, has all the fields of an L1 receipt and is the basis for the OP receipt.
272    pub core_receipt: TransactionReceipt<ReceiptWithBloom<OpReceipt<Log>>>,
273    /// Additional OP receipt fields.
274    pub op_receipt_fields: OpTransactionReceiptFields,
275}
276
277impl OpReceiptBuilder {
278    /// Returns a new builder.
279    pub fn new<N>(
280        chain_spec: &impl OpHardforks,
281        input: ConvertReceiptInput<'_, N>,
282        l1_block_info: &mut op_revm::L1BlockInfo,
283    ) -> Result<Self, OpEthApiError>
284    where
285        N: NodePrimitives<SignedTx: OpTransaction, Receipt = OpReceipt>,
286    {
287        let timestamp = input.meta.timestamp;
288        let block_number = input.meta.block_number;
289        let tx_signed = *input.tx.inner();
290        let mut core_receipt = build_receipt(input, None, |receipt, next_log_index, meta| {
291            let map_logs = move |receipt: alloy_consensus::Receipt| {
292                let Receipt { status, cumulative_gas_used, logs } = receipt;
293                let logs = Log::collect_for_receipt(next_log_index, meta, logs);
294                Receipt { status, cumulative_gas_used, logs }
295            };
296            let mapped_receipt: OpReceipt<Log> = match receipt {
297                OpReceipt::Legacy(receipt) => OpReceipt::Legacy(map_logs(receipt)),
298                OpReceipt::Eip2930(receipt) => OpReceipt::Eip2930(map_logs(receipt)),
299                OpReceipt::Eip1559(receipt) => OpReceipt::Eip1559(map_logs(receipt)),
300                OpReceipt::Eip7702(receipt) => OpReceipt::Eip7702(map_logs(receipt)),
301                OpReceipt::Deposit(receipt) => OpReceipt::Deposit(receipt.map_inner(map_logs)),
302            };
303            mapped_receipt.into_with_bloom()
304        });
305
306        // In jovian, we're using the blob gas used field to store the current da
307        // footprint's value.
308        // We're computing the jovian blob gas used before building the receipt since the inputs get
309        // consumed by the `build_receipt` function.
310        chain_spec.is_jovian_active_at_timestamp(timestamp).then(|| {
311            // Estimate the size of the transaction in bytes and multiply by the DA
312            // footprint gas scalar.
313            // Jovian specs: `https://github.com/ethereum-optimism/specs/blob/main/specs/protocol/jovian/exec-engine.md#da-footprint-block-limit`
314            let da_size = estimate_tx_compressed_size(tx_signed.encoded_2718().as_slice())
315                .saturating_div(1_000_000)
316                .saturating_mul(l1_block_info.da_footprint_gas_scalar.unwrap_or_default().into());
317
318            core_receipt.blob_gas_used = Some(da_size);
319        });
320
321        let op_receipt_fields = OpReceiptFieldsBuilder::new(timestamp, block_number)
322            .l1_block_info(chain_spec, tx_signed, l1_block_info)?
323            .build();
324
325        Ok(Self { core_receipt, op_receipt_fields })
326    }
327
328    /// Builds [`OpTransactionReceipt`] by combining core (l1) receipt fields and additional OP
329    /// receipt fields.
330    pub fn build(self) -> OpTransactionReceipt {
331        let Self { core_receipt: inner, op_receipt_fields } = self;
332
333        let OpTransactionReceiptFields { l1_block_info, .. } = op_receipt_fields;
334
335        OpTransactionReceipt { inner, l1_block_info }
336    }
337}
338
339#[cfg(test)]
340mod test {
341    use super::*;
342    use alloy_consensus::{transaction::TransactionMeta, Block, BlockBody, Eip658Value, TxEip7702};
343    use alloy_op_hardforks::{
344        OpChainHardforks, OP_MAINNET_ISTHMUS_TIMESTAMP, OP_MAINNET_JOVIAN_TIMESTAMP,
345    };
346    use alloy_primitives::{hex, Address, Bytes, Signature, U256};
347    use op_alloy_consensus::OpTypedTransaction;
348    use op_alloy_network::eip2718::Decodable2718;
349    use reth_optimism_chainspec::{BASE_MAINNET, OP_MAINNET};
350    use reth_optimism_primitives::{OpPrimitives, OpTransactionSigned};
351    use reth_primitives_traits::Recovered;
352
353    /// OP Mainnet transaction at index 0 in block 124665056.
354    ///
355    /// <https://optimistic.etherscan.io/tx/0x312e290cf36df704a2217b015d6455396830b0ce678b860ebfcc30f41403d7b1>
356    const TX_SET_L1_BLOCK_OP_MAINNET_BLOCK_124665056: [u8; 251] = hex!(
357        "7ef8f8a0683079df94aa5b9cf86687d739a60a9b4f0835e520ec4d664e2e415dca17a6df94deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e200000146b000f79c500000000000000040000000066d052e700000000013ad8a3000000000000000000000000000000000000000000000000000000003ef1278700000000000000000000000000000000000000000000000000000000000000012fdf87b89884a61e74b322bbcf60386f543bfae7827725efaaf0ab1de2294a590000000000000000000000006887246668a3b87f54deb3b94ba47a6f63f32985"
358    );
359
360    /// OP Mainnet transaction at index 1 in block 124665056.
361    ///
362    /// <https://optimistic.etherscan.io/tx/0x1059e8004daff32caa1f1b1ef97fe3a07a8cf40508f5b835b66d9420d87c4a4a>
363    const TX_1_OP_MAINNET_BLOCK_124665056: [u8; 1176] = hex!(
364        "02f904940a8303fba78401d6d2798401db2b6d830493e0943e6f4f7866654c18f536170780344aa8772950b680b904246a761202000000000000000000000000087000a300de7200382b55d40045000000e5d60e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003a0000000000000000000000000000000000000000000000000000000000000022482ad56cb0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000120000000000000000000000000dc6ff44d5d932cbd77b52e5612ba0529dc6226f1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000021c4928109acb0659a88ae5329b5374a3024694c0000000000000000000000000000000000000000000000049b9ca9a6943400000000000000000000000000000000000000000000000000000000000000000000000000000000000021c4928109acb0659a88ae5329b5374a3024694c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000024b6b55f250000000000000000000000000000000000000000000000049b9ca9a694340000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000415ec214a3950bea839a7e6fbb0ba1540ac2076acd50820e2d5ef83d0902cdffb24a47aff7de5190290769c4f0a9c6fabf63012986a0d590b1b571547a8c7050ea1b00000000000000000000000000000000000000000000000000000000000000c080a06db770e6e25a617fe9652f0958bd9bd6e49281a53036906386ed39ec48eadf63a07f47cf51a4a40b4494cf26efc686709a9b03939e20ee27e59682f5faa536667e"
365    );
366
367    /// Timestamp of OP mainnet block 124665056.
368    ///
369    /// <https://optimistic.etherscan.io/block/124665056>
370    const BLOCK_124665056_TIMESTAMP: u64 = 1724928889;
371
372    /// L1 block info for transaction at index 1 in block 124665056.
373    ///
374    /// <https://optimistic.etherscan.io/tx/0x1059e8004daff32caa1f1b1ef97fe3a07a8cf40508f5b835b66d9420d87c4a4a>
375    const TX_META_TX_1_OP_MAINNET_BLOCK_124665056: OpTransactionReceiptFields =
376        OpTransactionReceiptFields {
377            l1_block_info: L1BlockInfo {
378                l1_gas_price: Some(1055991687), // since bedrock l1 base fee
379                l1_gas_used: Some(4471),
380                l1_fee: Some(24681034813),
381                l1_fee_scalar: None,
382                l1_base_fee_scalar: Some(5227),
383                l1_blob_base_fee: Some(1),
384                l1_blob_base_fee_scalar: Some(1014213),
385                operator_fee_scalar: None,
386                operator_fee_constant: None,
387                da_footprint_gas_scalar: None,
388            },
389            deposit_nonce: None,
390            deposit_receipt_version: None,
391        };
392
393    #[test]
394    fn op_receipt_fields_from_block_and_tx() {
395        // rig
396        let tx_0 = OpTransactionSigned::decode_2718(
397            &mut TX_SET_L1_BLOCK_OP_MAINNET_BLOCK_124665056.as_slice(),
398        )
399        .unwrap();
400
401        let tx_1 =
402            OpTransactionSigned::decode_2718(&mut TX_1_OP_MAINNET_BLOCK_124665056.as_slice())
403                .unwrap();
404
405        let block: Block<OpTransactionSigned> = Block {
406            body: BlockBody { transactions: [tx_0, tx_1.clone()].to_vec(), ..Default::default() },
407            ..Default::default()
408        };
409
410        let mut l1_block_info =
411            reth_optimism_evm::extract_l1_info(&block.body).expect("should extract l1 info");
412
413        // test
414        assert!(OP_MAINNET.is_fjord_active_at_timestamp(BLOCK_124665056_TIMESTAMP));
415
416        let receipt_meta = OpReceiptFieldsBuilder::new(BLOCK_124665056_TIMESTAMP, 124665056)
417            .l1_block_info(&*OP_MAINNET, &tx_1, &mut l1_block_info)
418            .expect("should parse revm l1 info")
419            .build();
420
421        let L1BlockInfo {
422            l1_gas_price,
423            l1_gas_used,
424            l1_fee,
425            l1_fee_scalar,
426            l1_base_fee_scalar,
427            l1_blob_base_fee,
428            l1_blob_base_fee_scalar,
429            operator_fee_scalar,
430            operator_fee_constant,
431            da_footprint_gas_scalar,
432        } = receipt_meta.l1_block_info;
433
434        assert_eq!(
435            l1_gas_price, TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_gas_price,
436            "incorrect l1 base fee (former gas price)"
437        );
438        assert_eq!(
439            l1_gas_used, TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_gas_used,
440            "incorrect l1 gas used"
441        );
442        assert_eq!(
443            l1_fee, TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_fee,
444            "incorrect l1 fee"
445        );
446        assert_eq!(
447            l1_fee_scalar, TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_fee_scalar,
448            "incorrect l1 fee scalar"
449        );
450        assert_eq!(
451            l1_base_fee_scalar,
452            TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_base_fee_scalar,
453            "incorrect l1 base fee scalar"
454        );
455        assert_eq!(
456            l1_blob_base_fee,
457            TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_blob_base_fee,
458            "incorrect l1 blob base fee"
459        );
460        assert_eq!(
461            l1_blob_base_fee_scalar,
462            TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_blob_base_fee_scalar,
463            "incorrect l1 blob base fee scalar"
464        );
465        assert_eq!(
466            operator_fee_scalar,
467            TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.operator_fee_scalar,
468            "incorrect operator fee scalar"
469        );
470        assert_eq!(
471            operator_fee_constant,
472            TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.operator_fee_constant,
473            "incorrect operator fee constant"
474        );
475        assert_eq!(
476            da_footprint_gas_scalar,
477            TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.da_footprint_gas_scalar,
478            "incorrect da footprint gas scalar"
479        );
480    }
481
482    #[test]
483    fn op_non_zero_operator_fee_params_included_in_receipt() {
484        let tx_1 =
485            OpTransactionSigned::decode_2718(&mut TX_1_OP_MAINNET_BLOCK_124665056.as_slice())
486                .unwrap();
487
488        let mut l1_block_info = op_revm::L1BlockInfo {
489            operator_fee_scalar: Some(U256::ZERO),
490            operator_fee_constant: Some(U256::from(2)),
491            ..Default::default()
492        };
493
494        let receipt_meta = OpReceiptFieldsBuilder::new(BLOCK_124665056_TIMESTAMP, 124665056)
495            .l1_block_info(&*OP_MAINNET, &tx_1, &mut l1_block_info)
496            .expect("should parse revm l1 info")
497            .build();
498
499        let L1BlockInfo { operator_fee_scalar, operator_fee_constant, .. } =
500            receipt_meta.l1_block_info;
501
502        assert_eq!(operator_fee_scalar, Some(0), "incorrect operator fee scalar");
503        assert_eq!(operator_fee_constant, Some(2), "incorrect operator fee constant");
504    }
505
506    #[test]
507    fn op_zero_operator_fee_params_not_included_in_receipt() {
508        let tx_1 =
509            OpTransactionSigned::decode_2718(&mut TX_1_OP_MAINNET_BLOCK_124665056.as_slice())
510                .unwrap();
511
512        let mut l1_block_info = op_revm::L1BlockInfo {
513            operator_fee_scalar: Some(U256::ZERO),
514            operator_fee_constant: Some(U256::ZERO),
515            ..Default::default()
516        };
517
518        let receipt_meta = OpReceiptFieldsBuilder::new(BLOCK_124665056_TIMESTAMP, 124665056)
519            .l1_block_info(&*OP_MAINNET, &tx_1, &mut l1_block_info)
520            .expect("should parse revm l1 info")
521            .build();
522
523        let L1BlockInfo { operator_fee_scalar, operator_fee_constant, .. } =
524            receipt_meta.l1_block_info;
525
526        assert_eq!(operator_fee_scalar, None, "incorrect operator fee scalar");
527        assert_eq!(operator_fee_constant, None, "incorrect operator fee constant");
528    }
529
530    // <https://github.com/paradigmxyz/reth/issues/12177>
531    #[test]
532    fn base_receipt_gas_fields() {
533        // https://basescan.org/tx/0x510fd4c47d78ba9f97c91b0f2ace954d5384c169c9545a77a373cf3ef8254e6e
534        let system = hex!(
535            "7ef8f8a0389e292420bcbf9330741f72074e39562a09ff5a00fd22e4e9eee7e34b81bca494deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e20000008dd00101c120000000000000004000000006721035b00000000014189960000000000000000000000000000000000000000000000000000000349b4dcdc000000000000000000000000000000000000000000000000000000004ef9325cc5991ce750960f636ca2ffbb6e209bb3ba91412f21dd78c14ff154d1930f1f9a0000000000000000000000005050f69a9786f081509234f1a7f4684b5e5b76c9"
536        );
537        let tx_0 = OpTransactionSigned::decode_2718(&mut &system[..]).unwrap();
538
539        let block: alloy_consensus::Block<OpTransactionSigned> = Block {
540            body: BlockBody { transactions: vec![tx_0], ..Default::default() },
541            ..Default::default()
542        };
543        let mut l1_block_info =
544            reth_optimism_evm::extract_l1_info(&block.body).expect("should extract l1 info");
545
546        // https://basescan.org/tx/0xf9420cbaf66a2dda75a015488d37262cbfd4abd0aad7bb2be8a63e14b1fa7a94
547        let tx = hex!(
548            "02f86c8221058034839a4ae283021528942f16386bb37709016023232523ff6d9daf444be380841249c58bc080a001b927eda2af9b00b52a57be0885e0303c39dd2831732e14051c2336470fd468a0681bf120baf562915841a48601c2b54a6742511e535cf8f71c95115af7ff63bd"
549        );
550        let tx_1 = OpTransactionSigned::decode_2718(&mut &tx[..]).unwrap();
551
552        let receipt_meta = OpReceiptFieldsBuilder::new(1730216981, 21713817)
553            .l1_block_info(&*BASE_MAINNET, &tx_1, &mut l1_block_info)
554            .expect("should parse revm l1 info")
555            .build();
556
557        let L1BlockInfo {
558            l1_gas_price,
559            l1_gas_used,
560            l1_fee,
561            l1_fee_scalar,
562            l1_base_fee_scalar,
563            l1_blob_base_fee,
564            l1_blob_base_fee_scalar,
565            operator_fee_scalar,
566            operator_fee_constant,
567            da_footprint_gas_scalar,
568        } = receipt_meta.l1_block_info;
569
570        assert_eq!(l1_gas_price, Some(14121491676), "incorrect l1 base fee (former gas price)");
571        assert_eq!(l1_gas_used, Some(1600), "incorrect l1 gas used");
572        assert_eq!(l1_fee, Some(191150293412), "incorrect l1 fee");
573        assert!(l1_fee_scalar.is_none(), "incorrect l1 fee scalar");
574        assert_eq!(l1_base_fee_scalar, Some(2269), "incorrect l1 base fee scalar");
575        assert_eq!(l1_blob_base_fee, Some(1324954204), "incorrect l1 blob base fee");
576        assert_eq!(l1_blob_base_fee_scalar, Some(1055762), "incorrect l1 blob base fee scalar");
577        assert_eq!(operator_fee_scalar, None, "incorrect operator fee scalar");
578        assert_eq!(operator_fee_constant, None, "incorrect operator fee constant");
579        assert_eq!(da_footprint_gas_scalar, None, "incorrect da footprint gas scalar");
580    }
581
582    #[test]
583    fn da_footprint_gas_scalar_included_in_receipt_post_jovian() {
584        const DA_FOOTPRINT_GAS_SCALAR: u16 = 10;
585
586        let tx = TxEip7702 {
587            chain_id: 1u64,
588            nonce: 0,
589            max_fee_per_gas: 0x28f000fff,
590            max_priority_fee_per_gas: 0x28f000fff,
591            gas_limit: 10,
592            to: Address::default(),
593            value: U256::from(3_u64),
594            input: Bytes::from(vec![1, 2]),
595            access_list: Default::default(),
596            authorization_list: Default::default(),
597        };
598
599        let signature = Signature::new(U256::default(), U256::default(), true);
600
601        let tx = OpTransactionSigned::new_unhashed(OpTypedTransaction::Eip7702(tx), signature);
602
603        let mut l1_block_info = op_revm::L1BlockInfo {
604            da_footprint_gas_scalar: Some(DA_FOOTPRINT_GAS_SCALAR),
605            ..Default::default()
606        };
607
608        let op_hardforks = OpChainHardforks::op_mainnet();
609
610        let receipt = OpReceiptFieldsBuilder::new(OP_MAINNET_JOVIAN_TIMESTAMP, u64::MAX)
611            .l1_block_info(&op_hardforks, &tx, &mut l1_block_info)
612            .expect("should parse revm l1 info")
613            .build();
614
615        assert_eq!(receipt.l1_block_info.da_footprint_gas_scalar, Some(DA_FOOTPRINT_GAS_SCALAR));
616    }
617
618    #[test]
619    fn blob_gas_used_included_in_receipt_post_jovian() {
620        const DA_FOOTPRINT_GAS_SCALAR: u16 = 100;
621        let tx = TxEip7702 {
622            chain_id: 1u64,
623            nonce: 0,
624            max_fee_per_gas: 0x28f000fff,
625            max_priority_fee_per_gas: 0x28f000fff,
626            gas_limit: 10,
627            to: Address::default(),
628            value: U256::from(3_u64),
629            access_list: Default::default(),
630            authorization_list: Default::default(),
631            input: Bytes::from(vec![0; 1_000_000]),
632        };
633
634        let signature = Signature::new(U256::default(), U256::default(), true);
635
636        let tx = OpTransactionSigned::new_unhashed(OpTypedTransaction::Eip7702(tx), signature);
637
638        let mut l1_block_info = op_revm::L1BlockInfo {
639            da_footprint_gas_scalar: Some(DA_FOOTPRINT_GAS_SCALAR),
640            ..Default::default()
641        };
642
643        let op_hardforks = OpChainHardforks::op_mainnet();
644
645        let op_receipt = OpReceiptBuilder::new(
646            &op_hardforks,
647            ConvertReceiptInput::<OpPrimitives> {
648                tx: Recovered::new_unchecked(&tx, Address::default()),
649                receipt: OpReceipt::Eip7702(Receipt {
650                    status: Eip658Value::Eip658(true),
651                    cumulative_gas_used: 100,
652                    logs: vec![],
653                }),
654                gas_used: 100,
655                next_log_index: 0,
656                meta: TransactionMeta {
657                    timestamp: OP_MAINNET_JOVIAN_TIMESTAMP,
658                    ..Default::default()
659                },
660            },
661            &mut l1_block_info,
662        )
663        .unwrap();
664
665        let expected_blob_gas_used = estimate_tx_compressed_size(tx.encoded_2718().as_slice())
666            .saturating_div(1_000_000)
667            .saturating_mul(DA_FOOTPRINT_GAS_SCALAR.into());
668
669        assert_eq!(op_receipt.core_receipt.blob_gas_used, Some(expected_blob_gas_used));
670    }
671
672    #[test]
673    fn blob_gas_used_not_included_in_receipt_post_isthmus() {
674        const DA_FOOTPRINT_GAS_SCALAR: u16 = 100;
675        let tx = TxEip7702 {
676            chain_id: 1u64,
677            nonce: 0,
678            max_fee_per_gas: 0x28f000fff,
679            max_priority_fee_per_gas: 0x28f000fff,
680            gas_limit: 10,
681            to: Address::default(),
682            value: U256::from(3_u64),
683            access_list: Default::default(),
684            authorization_list: Default::default(),
685            input: Bytes::from(vec![0; 1_000_000]),
686        };
687
688        let signature = Signature::new(U256::default(), U256::default(), true);
689
690        let tx = OpTransactionSigned::new_unhashed(OpTypedTransaction::Eip7702(tx), signature);
691
692        let mut l1_block_info = op_revm::L1BlockInfo {
693            da_footprint_gas_scalar: Some(DA_FOOTPRINT_GAS_SCALAR),
694            ..Default::default()
695        };
696
697        let op_hardforks = OpChainHardforks::op_mainnet();
698
699        let op_receipt = OpReceiptBuilder::new(
700            &op_hardforks,
701            ConvertReceiptInput::<OpPrimitives> {
702                tx: Recovered::new_unchecked(&tx, Address::default()),
703                receipt: OpReceipt::Eip7702(Receipt {
704                    status: Eip658Value::Eip658(true),
705                    cumulative_gas_used: 100,
706                    logs: vec![],
707                }),
708                gas_used: 100,
709                next_log_index: 0,
710                meta: TransactionMeta {
711                    timestamp: OP_MAINNET_ISTHMUS_TIMESTAMP,
712                    ..Default::default()
713                },
714            },
715            &mut l1_block_info,
716        )
717        .unwrap();
718
719        assert_eq!(op_receipt.core_receipt.blob_gas_used, None);
720    }
721}