1use alloy_consensus::transaction::TransactionMeta;
4use alloy_eips::eip2718::Encodable2718;
5use alloy_rpc_types_eth::{Log, TransactionReceipt};
6use op_alloy_consensus::{OpDepositReceipt, OpDepositReceiptWithBloom, OpReceiptEnvelope};
7use op_alloy_rpc_types::{L1BlockInfo, OpTransactionReceipt, OpTransactionReceiptFields};
8use reth_chainspec::ChainSpecProvider;
9use reth_node_api::{FullNodeComponents, NodeTypes};
10use reth_optimism_chainspec::OpChainSpec;
11use reth_optimism_evm::RethL1BlockInfo;
12use reth_optimism_forks::OpHardforks;
13use reth_optimism_primitives::{OpReceipt, OpTransactionSigned};
14use reth_rpc_eth_api::{helpers::LoadReceipt, FromEthApiError, RpcReceipt};
15use reth_rpc_eth_types::{receipt::build_receipt, EthApiError};
16use reth_storage_api::{ReceiptProvider, TransactionsProvider};
17
18use crate::{OpEthApi, OpEthApiError};
19
20impl<N> LoadReceipt for OpEthApi<N>
21where
22 Self: Send + Sync,
23 N: FullNodeComponents<Types: NodeTypes<ChainSpec = OpChainSpec>>,
24 Self::Provider: TransactionsProvider<Transaction = OpTransactionSigned>
25 + ReceiptProvider<Receipt = OpReceipt>,
26{
27 async fn build_transaction_receipt(
28 &self,
29 tx: OpTransactionSigned,
30 meta: TransactionMeta,
31 receipt: OpReceipt,
32 ) -> Result<RpcReceipt<Self::NetworkTypes>, Self::Error> {
33 let (block, receipts) = self
34 .inner
35 .eth_api
36 .cache()
37 .get_block_and_receipts(meta.block_hash)
38 .await
39 .map_err(Self::Error::from_eth_err)?
40 .ok_or(Self::Error::from_eth_err(EthApiError::HeaderNotFound(
41 meta.block_hash.into(),
42 )))?;
43
44 let mut l1_block_info =
45 reth_optimism_evm::extract_l1_info(block.body()).map_err(OpEthApiError::from)?;
46
47 Ok(OpReceiptBuilder::new(
48 &self.inner.eth_api.provider().chain_spec(),
49 &tx,
50 meta,
51 &receipt,
52 &receipts,
53 &mut l1_block_info,
54 )?
55 .build())
56 }
57}
58
59#[derive(Debug, Clone)]
62pub struct OpReceiptFieldsBuilder {
63 pub block_number: u64,
65 pub block_timestamp: u64,
67 pub l1_fee: Option<u128>,
69 pub l1_data_gas: Option<u128>,
71 pub l1_fee_scalar: Option<f64>,
73 pub l1_base_fee: Option<u128>,
76 pub deposit_nonce: Option<u64>,
79 pub deposit_receipt_version: Option<u64>,
82 pub l1_base_fee_scalar: Option<u128>,
85 pub l1_blob_base_fee: Option<u128>,
87 pub l1_blob_base_fee_scalar: Option<u128>,
89 pub operator_fee_scalar: Option<u128>,
91 pub operator_fee_constant: Option<u128>,
93}
94
95impl OpReceiptFieldsBuilder {
96 pub const fn new(block_timestamp: u64, block_number: u64) -> Self {
98 Self {
99 block_number,
100 block_timestamp,
101 l1_fee: None,
102 l1_data_gas: None,
103 l1_fee_scalar: None,
104 l1_base_fee: None,
105 deposit_nonce: None,
106 deposit_receipt_version: None,
107 l1_base_fee_scalar: None,
108 l1_blob_base_fee: None,
109 l1_blob_base_fee_scalar: None,
110 operator_fee_scalar: None,
111 operator_fee_constant: None,
112 }
113 }
114
115 pub fn l1_block_info(
117 mut self,
118 chain_spec: &OpChainSpec,
119 tx: &OpTransactionSigned,
120 l1_block_info: &mut op_revm::L1BlockInfo,
121 ) -> Result<Self, OpEthApiError> {
122 let raw_tx = tx.encoded_2718();
123 let timestamp = self.block_timestamp;
124
125 self.l1_fee = Some(
126 l1_block_info
127 .l1_tx_data_fee(chain_spec, timestamp, &raw_tx, tx.is_deposit())
128 .map_err(|_| OpEthApiError::L1BlockFeeError)?
129 .saturating_to(),
130 );
131
132 self.l1_data_gas = Some(
133 l1_block_info
134 .l1_data_gas(chain_spec, timestamp, &raw_tx)
135 .map_err(|_| OpEthApiError::L1BlockGasError)?
136 .saturating_add(l1_block_info.l1_fee_overhead.unwrap_or_default())
137 .saturating_to(),
138 );
139
140 self.l1_fee_scalar = (!chain_spec.is_ecotone_active_at_timestamp(timestamp))
141 .then_some(f64::from(l1_block_info.l1_base_fee_scalar) / 1_000_000.0);
142
143 self.l1_base_fee = Some(l1_block_info.l1_base_fee.saturating_to());
144 self.l1_base_fee_scalar = Some(l1_block_info.l1_base_fee_scalar.saturating_to());
145 self.l1_blob_base_fee = l1_block_info.l1_blob_base_fee.map(|fee| fee.saturating_to());
146 self.l1_blob_base_fee_scalar =
147 l1_block_info.l1_blob_base_fee_scalar.map(|scalar| scalar.saturating_to());
148
149 let operator_fee_scalar_has_non_zero_value: bool =
151 l1_block_info.operator_fee_scalar.is_some_and(|scalar| !scalar.is_zero());
152
153 let operator_fee_constant_has_non_zero_value =
154 l1_block_info.operator_fee_constant.is_some_and(|constant| !constant.is_zero());
155
156 if operator_fee_scalar_has_non_zero_value || operator_fee_constant_has_non_zero_value {
157 self.operator_fee_scalar =
158 l1_block_info.operator_fee_scalar.map(|scalar| scalar.saturating_to());
159 self.operator_fee_constant =
160 l1_block_info.operator_fee_constant.map(|constant| constant.saturating_to());
161 }
162
163 Ok(self)
164 }
165
166 pub const fn deposit_nonce(mut self, nonce: Option<u64>) -> Self {
168 self.deposit_nonce = nonce;
169 self
170 }
171
172 pub const fn deposit_version(mut self, version: Option<u64>) -> Self {
174 self.deposit_receipt_version = version;
175 self
176 }
177
178 pub const fn build(self) -> OpTransactionReceiptFields {
180 let Self {
181 block_number: _, block_timestamp: _, l1_fee,
184 l1_data_gas: l1_gas_used,
185 l1_fee_scalar,
186 l1_base_fee: l1_gas_price,
187 deposit_nonce,
188 deposit_receipt_version,
189 l1_base_fee_scalar,
190 l1_blob_base_fee,
191 l1_blob_base_fee_scalar,
192 operator_fee_scalar,
193 operator_fee_constant,
194 } = self;
195
196 OpTransactionReceiptFields {
197 l1_block_info: L1BlockInfo {
198 l1_gas_price,
199 l1_gas_used,
200 l1_fee,
201 l1_fee_scalar,
202 l1_base_fee_scalar,
203 l1_blob_base_fee,
204 l1_blob_base_fee_scalar,
205 operator_fee_scalar,
206 operator_fee_constant,
207 },
208 deposit_nonce,
209 deposit_receipt_version,
210 }
211 }
212}
213
214#[derive(Debug)]
216pub struct OpReceiptBuilder {
217 pub core_receipt: TransactionReceipt<OpReceiptEnvelope<Log>>,
219 pub op_receipt_fields: OpTransactionReceiptFields,
221}
222
223impl OpReceiptBuilder {
224 pub fn new(
226 chain_spec: &OpChainSpec,
227 transaction: &OpTransactionSigned,
228 meta: TransactionMeta,
229 receipt: &OpReceipt,
230 all_receipts: &[OpReceipt],
231 l1_block_info: &mut op_revm::L1BlockInfo,
232 ) -> Result<Self, OpEthApiError> {
233 let timestamp = meta.timestamp;
234 let block_number = meta.block_number;
235 let core_receipt =
236 build_receipt(transaction, meta, receipt, all_receipts, None, |receipt_with_bloom| {
237 match receipt {
238 OpReceipt::Legacy(_) => OpReceiptEnvelope::<Log>::Legacy(receipt_with_bloom),
239 OpReceipt::Eip2930(_) => OpReceiptEnvelope::<Log>::Eip2930(receipt_with_bloom),
240 OpReceipt::Eip1559(_) => OpReceiptEnvelope::<Log>::Eip1559(receipt_with_bloom),
241 OpReceipt::Eip7702(_) => OpReceiptEnvelope::<Log>::Eip7702(receipt_with_bloom),
242 OpReceipt::Deposit(receipt) => {
243 OpReceiptEnvelope::<Log>::Deposit(OpDepositReceiptWithBloom::<Log> {
244 receipt: OpDepositReceipt::<Log> {
245 inner: receipt_with_bloom.receipt,
246 deposit_nonce: receipt.deposit_nonce,
247 deposit_receipt_version: receipt.deposit_receipt_version,
248 },
249 logs_bloom: receipt_with_bloom.logs_bloom,
250 })
251 }
252 }
253 })?;
254
255 let op_receipt_fields = OpReceiptFieldsBuilder::new(timestamp, block_number)
256 .l1_block_info(chain_spec, transaction, l1_block_info)?
257 .build();
258
259 Ok(Self { core_receipt, op_receipt_fields })
260 }
261
262 pub fn build(self) -> OpTransactionReceipt {
265 let Self { core_receipt: inner, op_receipt_fields } = self;
266
267 let OpTransactionReceiptFields { l1_block_info, .. } = op_receipt_fields;
268
269 OpTransactionReceipt { inner, l1_block_info }
270 }
271}
272
273#[cfg(test)]
274mod test {
275 use super::*;
276 use alloy_consensus::{Block, BlockBody};
277 use alloy_primitives::{hex, U256};
278 use op_alloy_network::eip2718::Decodable2718;
279 use reth_optimism_chainspec::{BASE_MAINNET, OP_MAINNET};
280
281 const TX_SET_L1_BLOCK_OP_MAINNET_BLOCK_124665056: [u8; 251] = hex!(
285 "7ef8f8a0683079df94aa5b9cf86687d739a60a9b4f0835e520ec4d664e2e415dca17a6df94deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e200000146b000f79c500000000000000040000000066d052e700000000013ad8a3000000000000000000000000000000000000000000000000000000003ef1278700000000000000000000000000000000000000000000000000000000000000012fdf87b89884a61e74b322bbcf60386f543bfae7827725efaaf0ab1de2294a590000000000000000000000006887246668a3b87f54deb3b94ba47a6f63f32985"
286 );
287
288 const TX_1_OP_MAINNET_BLOCK_124665056: [u8; 1176] = hex!(
292 "02f904940a8303fba78401d6d2798401db2b6d830493e0943e6f4f7866654c18f536170780344aa8772950b680b904246a761202000000000000000000000000087000a300de7200382b55d40045000000e5d60e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003a0000000000000000000000000000000000000000000000000000000000000022482ad56cb0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000120000000000000000000000000dc6ff44d5d932cbd77b52e5612ba0529dc6226f1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000021c4928109acb0659a88ae5329b5374a3024694c0000000000000000000000000000000000000000000000049b9ca9a6943400000000000000000000000000000000000000000000000000000000000000000000000000000000000021c4928109acb0659a88ae5329b5374a3024694c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000024b6b55f250000000000000000000000000000000000000000000000049b9ca9a694340000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000415ec214a3950bea839a7e6fbb0ba1540ac2076acd50820e2d5ef83d0902cdffb24a47aff7de5190290769c4f0a9c6fabf63012986a0d590b1b571547a8c7050ea1b00000000000000000000000000000000000000000000000000000000000000c080a06db770e6e25a617fe9652f0958bd9bd6e49281a53036906386ed39ec48eadf63a07f47cf51a4a40b4494cf26efc686709a9b03939e20ee27e59682f5faa536667e"
293 );
294
295 const BLOCK_124665056_TIMESTAMP: u64 = 1724928889;
299
300 const TX_META_TX_1_OP_MAINNET_BLOCK_124665056: OpTransactionReceiptFields =
304 OpTransactionReceiptFields {
305 l1_block_info: L1BlockInfo {
306 l1_gas_price: Some(1055991687), l1_gas_used: Some(4471),
308 l1_fee: Some(24681034813),
309 l1_fee_scalar: None,
310 l1_base_fee_scalar: Some(5227),
311 l1_blob_base_fee: Some(1),
312 l1_blob_base_fee_scalar: Some(1014213),
313 operator_fee_scalar: None,
314 operator_fee_constant: None,
315 },
316 deposit_nonce: None,
317 deposit_receipt_version: None,
318 };
319
320 #[test]
321 fn op_receipt_fields_from_block_and_tx() {
322 let tx_0 = OpTransactionSigned::decode_2718(
324 &mut TX_SET_L1_BLOCK_OP_MAINNET_BLOCK_124665056.as_slice(),
325 )
326 .unwrap();
327
328 let tx_1 =
329 OpTransactionSigned::decode_2718(&mut TX_1_OP_MAINNET_BLOCK_124665056.as_slice())
330 .unwrap();
331
332 let block: Block<OpTransactionSigned> = Block {
333 body: BlockBody { transactions: [tx_0, tx_1.clone()].to_vec(), ..Default::default() },
334 ..Default::default()
335 };
336
337 let mut l1_block_info =
338 reth_optimism_evm::extract_l1_info(&block.body).expect("should extract l1 info");
339
340 assert!(OP_MAINNET.is_fjord_active_at_timestamp(BLOCK_124665056_TIMESTAMP));
342
343 let receipt_meta = OpReceiptFieldsBuilder::new(BLOCK_124665056_TIMESTAMP, 124665056)
344 .l1_block_info(&OP_MAINNET, &tx_1, &mut l1_block_info)
345 .expect("should parse revm l1 info")
346 .build();
347
348 let L1BlockInfo {
349 l1_gas_price,
350 l1_gas_used,
351 l1_fee,
352 l1_fee_scalar,
353 l1_base_fee_scalar,
354 l1_blob_base_fee,
355 l1_blob_base_fee_scalar,
356 operator_fee_scalar,
357 operator_fee_constant,
358 } = receipt_meta.l1_block_info;
359
360 assert_eq!(
361 l1_gas_price, TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_gas_price,
362 "incorrect l1 base fee (former gas price)"
363 );
364 assert_eq!(
365 l1_gas_used, TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_gas_used,
366 "incorrect l1 gas used"
367 );
368 assert_eq!(
369 l1_fee, TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_fee,
370 "incorrect l1 fee"
371 );
372 assert_eq!(
373 l1_fee_scalar, TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_fee_scalar,
374 "incorrect l1 fee scalar"
375 );
376 assert_eq!(
377 l1_base_fee_scalar,
378 TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_base_fee_scalar,
379 "incorrect l1 base fee scalar"
380 );
381 assert_eq!(
382 l1_blob_base_fee,
383 TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_blob_base_fee,
384 "incorrect l1 blob base fee"
385 );
386 assert_eq!(
387 l1_blob_base_fee_scalar,
388 TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_blob_base_fee_scalar,
389 "incorrect l1 blob base fee scalar"
390 );
391 assert_eq!(
392 operator_fee_scalar,
393 TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.operator_fee_scalar,
394 "incorrect operator fee scalar"
395 );
396 assert_eq!(
397 operator_fee_constant,
398 TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.operator_fee_constant,
399 "incorrect operator fee constant"
400 );
401 }
402
403 #[test]
404 fn op_non_zero_operator_fee_params_included_in_receipt() {
405 let tx_1 =
406 OpTransactionSigned::decode_2718(&mut TX_1_OP_MAINNET_BLOCK_124665056.as_slice())
407 .unwrap();
408
409 let mut l1_block_info = op_revm::L1BlockInfo::default();
410
411 l1_block_info.operator_fee_scalar = Some(U256::ZERO);
412 l1_block_info.operator_fee_constant = Some(U256::from(2));
413
414 let receipt_meta = OpReceiptFieldsBuilder::new(BLOCK_124665056_TIMESTAMP, 124665056)
415 .l1_block_info(&OP_MAINNET, &tx_1, &mut l1_block_info)
416 .expect("should parse revm l1 info")
417 .build();
418
419 let L1BlockInfo { operator_fee_scalar, operator_fee_constant, .. } =
420 receipt_meta.l1_block_info;
421
422 assert_eq!(operator_fee_scalar, Some(0), "incorrect operator fee scalar");
423 assert_eq!(operator_fee_constant, Some(2), "incorrect operator fee constant");
424 }
425
426 #[test]
427 fn op_zero_operator_fee_params_not_included_in_receipt() {
428 let tx_1 =
429 OpTransactionSigned::decode_2718(&mut TX_1_OP_MAINNET_BLOCK_124665056.as_slice())
430 .unwrap();
431
432 let mut l1_block_info = op_revm::L1BlockInfo::default();
433
434 l1_block_info.operator_fee_scalar = Some(U256::ZERO);
435 l1_block_info.operator_fee_constant = Some(U256::ZERO);
436
437 let receipt_meta = OpReceiptFieldsBuilder::new(BLOCK_124665056_TIMESTAMP, 124665056)
438 .l1_block_info(&OP_MAINNET, &tx_1, &mut l1_block_info)
439 .expect("should parse revm l1 info")
440 .build();
441
442 let L1BlockInfo { operator_fee_scalar, operator_fee_constant, .. } =
443 receipt_meta.l1_block_info;
444
445 assert_eq!(operator_fee_scalar, None, "incorrect operator fee scalar");
446 assert_eq!(operator_fee_constant, None, "incorrect operator fee constant");
447 }
448
449 #[test]
451 fn base_receipt_gas_fields() {
452 let system = hex!(
454 "7ef8f8a0389e292420bcbf9330741f72074e39562a09ff5a00fd22e4e9eee7e34b81bca494deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e20000008dd00101c120000000000000004000000006721035b00000000014189960000000000000000000000000000000000000000000000000000000349b4dcdc000000000000000000000000000000000000000000000000000000004ef9325cc5991ce750960f636ca2ffbb6e209bb3ba91412f21dd78c14ff154d1930f1f9a0000000000000000000000005050f69a9786f081509234f1a7f4684b5e5b76c9"
455 );
456 let tx_0 = OpTransactionSigned::decode_2718(&mut &system[..]).unwrap();
457
458 let block: alloy_consensus::Block<OpTransactionSigned> = Block {
459 body: BlockBody { transactions: vec![tx_0], ..Default::default() },
460 ..Default::default()
461 };
462 let mut l1_block_info =
463 reth_optimism_evm::extract_l1_info(&block.body).expect("should extract l1 info");
464
465 let tx = hex!(
467 "02f86c8221058034839a4ae283021528942f16386bb37709016023232523ff6d9daf444be380841249c58bc080a001b927eda2af9b00b52a57be0885e0303c39dd2831732e14051c2336470fd468a0681bf120baf562915841a48601c2b54a6742511e535cf8f71c95115af7ff63bd"
468 );
469 let tx_1 = OpTransactionSigned::decode_2718(&mut &tx[..]).unwrap();
470
471 let receipt_meta = OpReceiptFieldsBuilder::new(1730216981, 21713817)
472 .l1_block_info(&BASE_MAINNET, &tx_1, &mut l1_block_info)
473 .expect("should parse revm l1 info")
474 .build();
475
476 let L1BlockInfo {
477 l1_gas_price,
478 l1_gas_used,
479 l1_fee,
480 l1_fee_scalar,
481 l1_base_fee_scalar,
482 l1_blob_base_fee,
483 l1_blob_base_fee_scalar,
484 operator_fee_scalar,
485 operator_fee_constant,
486 } = receipt_meta.l1_block_info;
487
488 assert_eq!(l1_gas_price, Some(14121491676), "incorrect l1 base fee (former gas price)");
489 assert_eq!(l1_gas_used, Some(1600), "incorrect l1 gas used");
490 assert_eq!(l1_fee, Some(191150293412), "incorrect l1 fee");
491 assert!(l1_fee_scalar.is_none(), "incorrect l1 fee scalar");
492 assert_eq!(l1_base_fee_scalar, Some(2269), "incorrect l1 base fee scalar");
493 assert_eq!(l1_blob_base_fee, Some(1324954204), "incorrect l1 blob base fee");
494 assert_eq!(l1_blob_base_fee_scalar, Some(1055762), "incorrect l1 blob base fee scalar");
495 assert_eq!(operator_fee_scalar, None, "incorrect operator fee scalar");
496 assert_eq!(operator_fee_constant, None, "incorrect operator fee constant");
497 }
498}