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