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