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_node_api::{FullNodeComponents, NodeTypes};
9use reth_optimism_chainspec::OpChainSpec;
10use reth_optimism_evm::RethL1BlockInfo;
11use reth_optimism_forks::OpHardforks;
12use reth_optimism_primitives::{OpReceipt, OpTransactionSigned};
13use reth_provider::{ChainSpecProvider, ReceiptProvider, TransactionsProvider};
14use reth_rpc_eth_api::{helpers::LoadReceipt, FromEthApiError, RpcReceipt};
15use reth_rpc_eth_types::{receipt::build_receipt, EthApiError};
16
17use crate::{OpEthApi, OpEthApiError};
18
19impl<N> LoadReceipt for OpEthApi<N>
20where
21 Self: Send + Sync,
22 N: FullNodeComponents<Types: NodeTypes<ChainSpec = OpChainSpec>>,
23 Self::Provider: TransactionsProvider<Transaction = OpTransactionSigned>
24 + ReceiptProvider<Receipt = OpReceipt>,
25{
26 async fn build_transaction_receipt(
27 &self,
28 tx: OpTransactionSigned,
29 meta: TransactionMeta,
30 receipt: OpReceipt,
31 ) -> Result<RpcReceipt<Self::NetworkTypes>, Self::Error> {
32 let (block, receipts) = self
33 .inner
34 .eth_api
35 .cache()
36 .get_block_and_receipts(meta.block_hash)
37 .await
38 .map_err(Self::Error::from_eth_err)?
39 .ok_or(Self::Error::from_eth_err(EthApiError::HeaderNotFound(
40 meta.block_hash.into(),
41 )))?;
42
43 let mut l1_block_info =
44 reth_optimism_evm::extract_l1_info(block.body()).map_err(OpEthApiError::from)?;
45
46 Ok(OpReceiptBuilder::new(
47 &self.inner.eth_api.provider().chain_spec(),
48 &tx,
49 meta,
50 &receipt,
51 &receipts,
52 &mut l1_block_info,
53 )?
54 .build())
55 }
56}
57
58#[derive(Debug, Clone)]
61pub struct OpReceiptFieldsBuilder {
62 pub block_number: u64,
64 pub block_timestamp: u64,
66 pub l1_fee: Option<u128>,
68 pub l1_data_gas: Option<u128>,
70 pub l1_fee_scalar: Option<f64>,
72 pub l1_base_fee: Option<u128>,
75 pub deposit_nonce: Option<u64>,
78 pub deposit_receipt_version: Option<u64>,
81 pub l1_base_fee_scalar: Option<u128>,
84 pub l1_blob_base_fee: Option<u128>,
86 pub l1_blob_base_fee_scalar: Option<u128>,
88 pub operator_fee_scalar: Option<u128>,
90 pub operator_fee_constant: Option<u128>,
92}
93
94impl OpReceiptFieldsBuilder {
95 pub const fn new(block_timestamp: u64, block_number: u64) -> Self {
97 Self {
98 block_number,
99 block_timestamp,
100 l1_fee: None,
101 l1_data_gas: None,
102 l1_fee_scalar: None,
103 l1_base_fee: None,
104 deposit_nonce: None,
105 deposit_receipt_version: None,
106 l1_base_fee_scalar: None,
107 l1_blob_base_fee: None,
108 l1_blob_base_fee_scalar: None,
109 operator_fee_scalar: None,
110 operator_fee_constant: None,
111 }
112 }
113
114 pub fn l1_block_info(
116 mut self,
117 chain_spec: &OpChainSpec,
118 tx: &OpTransactionSigned,
119 l1_block_info: &mut op_revm::L1BlockInfo,
120 ) -> Result<Self, OpEthApiError> {
121 let raw_tx = tx.encoded_2718();
122 let block_number = self.block_number;
123 let timestamp = self.block_timestamp;
124
125 self.l1_fee = Some(
126 l1_block_info
127 .l1_tx_data_fee(chain_spec, timestamp, block_number, &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, block_number, &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!("7ef8f8a0683079df94aa5b9cf86687d739a60a9b4f0835e520ec4d664e2e415dca17a6df94deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e200000146b000f79c500000000000000040000000066d052e700000000013ad8a3000000000000000000000000000000000000000000000000000000003ef1278700000000000000000000000000000000000000000000000000000000000000012fdf87b89884a61e74b322bbcf60386f543bfae7827725efaaf0ab1de2294a590000000000000000000000006887246668a3b87f54deb3b94ba47a6f63f32985");
285
286 const TX_1_OP_MAINNET_BLOCK_124665056: [u8; 1176] = hex!("02f904940a8303fba78401d6d2798401db2b6d830493e0943e6f4f7866654c18f536170780344aa8772950b680b904246a761202000000000000000000000000087000a300de7200382b55d40045000000e5d60e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003a0000000000000000000000000000000000000000000000000000000000000022482ad56cb0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000120000000000000000000000000dc6ff44d5d932cbd77b52e5612ba0529dc6226f1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000021c4928109acb0659a88ae5329b5374a3024694c0000000000000000000000000000000000000000000000049b9ca9a6943400000000000000000000000000000000000000000000000000000000000000000000000000000000000021c4928109acb0659a88ae5329b5374a3024694c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000024b6b55f250000000000000000000000000000000000000000000000049b9ca9a694340000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000415ec214a3950bea839a7e6fbb0ba1540ac2076acd50820e2d5ef83d0902cdffb24a47aff7de5190290769c4f0a9c6fabf63012986a0d590b1b571547a8c7050ea1b00000000000000000000000000000000000000000000000000000000000000c080a06db770e6e25a617fe9652f0958bd9bd6e49281a53036906386ed39ec48eadf63a07f47cf51a4a40b4494cf26efc686709a9b03939e20ee27e59682f5faa536667e");
290
291 const BLOCK_124665056_TIMESTAMP: u64 = 1724928889;
295
296 const TX_META_TX_1_OP_MAINNET_BLOCK_124665056: OpTransactionReceiptFields =
300 OpTransactionReceiptFields {
301 l1_block_info: L1BlockInfo {
302 l1_gas_price: Some(1055991687), l1_gas_used: Some(4471),
304 l1_fee: Some(24681034813),
305 l1_fee_scalar: None,
306 l1_base_fee_scalar: Some(5227),
307 l1_blob_base_fee: Some(1),
308 l1_blob_base_fee_scalar: Some(1014213),
309 operator_fee_scalar: None,
310 operator_fee_constant: None,
311 },
312 deposit_nonce: None,
313 deposit_receipt_version: None,
314 };
315
316 #[test]
317 fn op_receipt_fields_from_block_and_tx() {
318 let tx_0 = OpTransactionSigned::decode_2718(
320 &mut TX_SET_L1_BLOCK_OP_MAINNET_BLOCK_124665056.as_slice(),
321 )
322 .unwrap();
323
324 let tx_1 =
325 OpTransactionSigned::decode_2718(&mut TX_1_OP_MAINNET_BLOCK_124665056.as_slice())
326 .unwrap();
327
328 let block: Block<OpTransactionSigned> = Block {
329 body: BlockBody { transactions: [tx_0, tx_1.clone()].to_vec(), ..Default::default() },
330 ..Default::default()
331 };
332
333 let mut l1_block_info =
334 reth_optimism_evm::extract_l1_info(&block.body).expect("should extract l1 info");
335
336 assert!(OP_MAINNET.is_fjord_active_at_timestamp(BLOCK_124665056_TIMESTAMP));
338
339 let receipt_meta = OpReceiptFieldsBuilder::new(BLOCK_124665056_TIMESTAMP, 124665056)
340 .l1_block_info(&OP_MAINNET, &tx_1, &mut l1_block_info)
341 .expect("should parse revm l1 info")
342 .build();
343
344 let L1BlockInfo {
345 l1_gas_price,
346 l1_gas_used,
347 l1_fee,
348 l1_fee_scalar,
349 l1_base_fee_scalar,
350 l1_blob_base_fee,
351 l1_blob_base_fee_scalar,
352 operator_fee_scalar,
353 operator_fee_constant,
354 } = receipt_meta.l1_block_info;
355
356 assert_eq!(
357 l1_gas_price, TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_gas_price,
358 "incorrect l1 base fee (former gas price)"
359 );
360 assert_eq!(
361 l1_gas_used, TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_gas_used,
362 "incorrect l1 gas used"
363 );
364 assert_eq!(
365 l1_fee, TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_fee,
366 "incorrect l1 fee"
367 );
368 assert_eq!(
369 l1_fee_scalar, TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_fee_scalar,
370 "incorrect l1 fee scalar"
371 );
372 assert_eq!(
373 l1_base_fee_scalar,
374 TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_base_fee_scalar,
375 "incorrect l1 base fee scalar"
376 );
377 assert_eq!(
378 l1_blob_base_fee,
379 TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_blob_base_fee,
380 "incorrect l1 blob base fee"
381 );
382 assert_eq!(
383 l1_blob_base_fee_scalar,
384 TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_blob_base_fee_scalar,
385 "incorrect l1 blob base fee scalar"
386 );
387 assert_eq!(
388 operator_fee_scalar,
389 TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.operator_fee_scalar,
390 "incorrect operator fee scalar"
391 );
392 assert_eq!(
393 operator_fee_constant,
394 TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.operator_fee_constant,
395 "incorrect operator fee constant"
396 );
397 }
398
399 #[test]
400 fn op_non_zero_operator_fee_params_included_in_receipt() {
401 let tx_1 =
402 OpTransactionSigned::decode_2718(&mut TX_1_OP_MAINNET_BLOCK_124665056.as_slice())
403 .unwrap();
404
405 let mut l1_block_info = op_revm::L1BlockInfo::default();
406
407 l1_block_info.operator_fee_scalar = Some(U256::ZERO);
408 l1_block_info.operator_fee_constant = Some(U256::from(2));
409
410 let receipt_meta = OpReceiptFieldsBuilder::new(BLOCK_124665056_TIMESTAMP, 124665056)
411 .l1_block_info(&OP_MAINNET, &tx_1, &mut l1_block_info)
412 .expect("should parse revm l1 info")
413 .build();
414
415 let L1BlockInfo { operator_fee_scalar, operator_fee_constant, .. } =
416 receipt_meta.l1_block_info;
417
418 assert_eq!(operator_fee_scalar, Some(0), "incorrect operator fee scalar");
419 assert_eq!(operator_fee_constant, Some(2), "incorrect operator fee constant");
420 }
421
422 #[test]
423 fn op_zero_operator_fee_params_not_included_in_receipt() {
424 let tx_1 =
425 OpTransactionSigned::decode_2718(&mut TX_1_OP_MAINNET_BLOCK_124665056.as_slice())
426 .unwrap();
427
428 let mut l1_block_info = op_revm::L1BlockInfo::default();
429
430 l1_block_info.operator_fee_scalar = Some(U256::ZERO);
431 l1_block_info.operator_fee_constant = Some(U256::ZERO);
432
433 let receipt_meta = OpReceiptFieldsBuilder::new(BLOCK_124665056_TIMESTAMP, 124665056)
434 .l1_block_info(&OP_MAINNET, &tx_1, &mut l1_block_info)
435 .expect("should parse revm l1 info")
436 .build();
437
438 let L1BlockInfo { operator_fee_scalar, operator_fee_constant, .. } =
439 receipt_meta.l1_block_info;
440
441 assert_eq!(operator_fee_scalar, None, "incorrect operator fee scalar");
442 assert_eq!(operator_fee_constant, None, "incorrect operator fee constant");
443 }
444
445 #[test]
447 fn base_receipt_gas_fields() {
448 let system = hex!("7ef8f8a0389e292420bcbf9330741f72074e39562a09ff5a00fd22e4e9eee7e34b81bca494deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e20000008dd00101c120000000000000004000000006721035b00000000014189960000000000000000000000000000000000000000000000000000000349b4dcdc000000000000000000000000000000000000000000000000000000004ef9325cc5991ce750960f636ca2ffbb6e209bb3ba91412f21dd78c14ff154d1930f1f9a0000000000000000000000005050f69a9786f081509234f1a7f4684b5e5b76c9");
450 let tx_0 = OpTransactionSigned::decode_2718(&mut &system[..]).unwrap();
451
452 let block: alloy_consensus::Block<OpTransactionSigned> = Block {
453 body: BlockBody { transactions: vec![tx_0], ..Default::default() },
454 ..Default::default()
455 };
456 let mut l1_block_info =
457 reth_optimism_evm::extract_l1_info(&block.body).expect("should extract l1 info");
458
459 let tx = hex!("02f86c8221058034839a4ae283021528942f16386bb37709016023232523ff6d9daf444be380841249c58bc080a001b927eda2af9b00b52a57be0885e0303c39dd2831732e14051c2336470fd468a0681bf120baf562915841a48601c2b54a6742511e535cf8f71c95115af7ff63bd");
461 let tx_1 = OpTransactionSigned::decode_2718(&mut &tx[..]).unwrap();
462
463 let receipt_meta = OpReceiptFieldsBuilder::new(1730216981, 21713817)
464 .l1_block_info(&BASE_MAINNET, &tx_1, &mut l1_block_info)
465 .expect("should parse revm l1 info")
466 .build();
467
468 let L1BlockInfo {
469 l1_gas_price,
470 l1_gas_used,
471 l1_fee,
472 l1_fee_scalar,
473 l1_base_fee_scalar,
474 l1_blob_base_fee,
475 l1_blob_base_fee_scalar,
476 operator_fee_scalar,
477 operator_fee_constant,
478 } = receipt_meta.l1_block_info;
479
480 assert_eq!(l1_gas_price, Some(14121491676), "incorrect l1 base fee (former gas price)");
481 assert_eq!(l1_gas_used, Some(1600), "incorrect l1 gas used");
482 assert_eq!(l1_fee, Some(191150293412), "incorrect l1 fee");
483 assert!(l1_fee_scalar.is_none(), "incorrect l1 fee scalar");
484 assert_eq!(l1_base_fee_scalar, Some(2269), "incorrect l1 base fee scalar");
485 assert_eq!(l1_blob_base_fee, Some(1324954204), "incorrect l1 blob base fee");
486 assert_eq!(l1_blob_base_fee_scalar, Some(1055762), "incorrect l1 blob base fee scalar");
487 assert_eq!(operator_fee_scalar, None, "incorrect operator fee scalar");
488 assert_eq!(operator_fee_constant, None, "incorrect operator fee constant");
489 }
490}