1use 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 reth_chainspec::ChainSpecProvider;
10use reth_node_api::NodePrimitives;
11use reth_optimism_evm::RethL1BlockInfo;
12use reth_optimism_forks::OpHardforks;
13use reth_optimism_primitives::OpReceipt;
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#[derive(Debug, Clone)]
33pub struct OpReceiptConverter<Provider> {
34 provider: Provider,
35}
36
37impl<Provider> OpReceiptConverter<Provider> {
38 pub const fn new(provider: Provider) -> Self {
40 Self { provider }
41 }
42}
43
44impl<Provider, N> ReceiptConverter<N> for OpReceiptConverter<Provider>
45where
46 N: NodePrimitives<SignedTx: OpTransaction, Receipt = OpReceipt>,
47 Provider:
48 BlockReader<Block = N::Block> + ChainSpecProvider<ChainSpec: OpHardforks> + Debug + 'static,
49{
50 type RpcReceipt = OpTransactionReceipt;
51 type Error = OpEthApiError;
52
53 fn convert_receipts(
54 &self,
55 inputs: Vec<ConvertReceiptInput<'_, N>>,
56 ) -> Result<Vec<Self::RpcReceipt>, Self::Error> {
57 let Some(block_number) = inputs.first().map(|r| r.meta.block_number) else {
58 return Ok(Vec::new());
59 };
60
61 let block = self
62 .provider
63 .block_by_number(block_number)?
64 .ok_or(EthApiError::HeaderNotFound(block_number.into()))?;
65
66 self.convert_receipts_with_block(inputs, &SealedBlock::new_unhashed(block))
67 }
68
69 fn convert_receipts_with_block(
70 &self,
71 inputs: Vec<ConvertReceiptInput<'_, N>>,
72 block: &SealedBlock<N::Block>,
73 ) -> Result<Vec<Self::RpcReceipt>, Self::Error> {
74 let mut l1_block_info = match reth_optimism_evm::extract_l1_info(block.body()) {
75 Ok(l1_block_info) => l1_block_info,
76 Err(err) => {
77 if block.header().number() == 0 {
80 return Ok(vec![]);
81 }
82 return Err(err.into());
83 }
84 };
85
86 let mut receipts = Vec::with_capacity(inputs.len());
87
88 for input in inputs {
89 l1_block_info.clear_tx_l1_cost();
93
94 receipts.push(
95 OpReceiptBuilder::new(&self.provider.chain_spec(), input, &mut l1_block_info)?
96 .build(),
97 );
98 }
99
100 Ok(receipts)
101 }
102}
103
104#[derive(Debug, Clone)]
107pub struct OpReceiptFieldsBuilder {
108 pub block_number: u64,
110 pub block_timestamp: u64,
112 pub l1_fee: Option<u128>,
114 pub l1_data_gas: Option<u128>,
116 pub l1_fee_scalar: Option<f64>,
118 pub l1_base_fee: Option<u128>,
121 pub deposit_nonce: Option<u64>,
124 pub deposit_receipt_version: Option<u64>,
127 pub l1_base_fee_scalar: Option<u128>,
130 pub l1_blob_base_fee: Option<u128>,
132 pub l1_blob_base_fee_scalar: Option<u128>,
134 pub operator_fee_scalar: Option<u128>,
136 pub operator_fee_constant: Option<u128>,
138}
139
140impl OpReceiptFieldsBuilder {
141 pub const fn new(block_timestamp: u64, block_number: u64) -> Self {
143 Self {
144 block_number,
145 block_timestamp,
146 l1_fee: None,
147 l1_data_gas: None,
148 l1_fee_scalar: None,
149 l1_base_fee: None,
150 deposit_nonce: None,
151 deposit_receipt_version: None,
152 l1_base_fee_scalar: None,
153 l1_blob_base_fee: None,
154 l1_blob_base_fee_scalar: None,
155 operator_fee_scalar: None,
156 operator_fee_constant: None,
157 }
158 }
159
160 pub fn l1_block_info<T: Encodable2718 + OpTransaction>(
162 mut self,
163 chain_spec: &impl OpHardforks,
164 tx: &T,
165 l1_block_info: &mut op_revm::L1BlockInfo,
166 ) -> Result<Self, OpEthApiError> {
167 let raw_tx = tx.encoded_2718();
168 let timestamp = self.block_timestamp;
169
170 self.l1_fee = Some(
171 l1_block_info
172 .l1_tx_data_fee(chain_spec, timestamp, &raw_tx, tx.is_deposit())
173 .map_err(|_| OpEthApiError::L1BlockFeeError)?
174 .saturating_to(),
175 );
176
177 self.l1_data_gas = Some(
178 l1_block_info
179 .l1_data_gas(chain_spec, timestamp, &raw_tx)
180 .map_err(|_| OpEthApiError::L1BlockGasError)?
181 .saturating_add(l1_block_info.l1_fee_overhead.unwrap_or_default())
182 .saturating_to(),
183 );
184
185 self.l1_fee_scalar = (!chain_spec.is_ecotone_active_at_timestamp(timestamp))
186 .then_some(f64::from(l1_block_info.l1_base_fee_scalar) / 1_000_000.0);
187
188 self.l1_base_fee = Some(l1_block_info.l1_base_fee.saturating_to());
189 self.l1_base_fee_scalar = Some(l1_block_info.l1_base_fee_scalar.saturating_to());
190 self.l1_blob_base_fee = l1_block_info.l1_blob_base_fee.map(|fee| fee.saturating_to());
191 self.l1_blob_base_fee_scalar =
192 l1_block_info.l1_blob_base_fee_scalar.map(|scalar| scalar.saturating_to());
193
194 let operator_fee_scalar_has_non_zero_value: bool =
196 l1_block_info.operator_fee_scalar.is_some_and(|scalar| !scalar.is_zero());
197
198 let operator_fee_constant_has_non_zero_value =
199 l1_block_info.operator_fee_constant.is_some_and(|constant| !constant.is_zero());
200
201 if operator_fee_scalar_has_non_zero_value || operator_fee_constant_has_non_zero_value {
202 self.operator_fee_scalar =
203 l1_block_info.operator_fee_scalar.map(|scalar| scalar.saturating_to());
204 self.operator_fee_constant =
205 l1_block_info.operator_fee_constant.map(|constant| constant.saturating_to());
206 }
207
208 Ok(self)
209 }
210
211 pub const fn deposit_nonce(mut self, nonce: Option<u64>) -> Self {
213 self.deposit_nonce = nonce;
214 self
215 }
216
217 pub const fn deposit_version(mut self, version: Option<u64>) -> Self {
219 self.deposit_receipt_version = version;
220 self
221 }
222
223 pub const fn build(self) -> OpTransactionReceiptFields {
225 let Self {
226 block_number: _, block_timestamp: _, l1_fee,
229 l1_data_gas: l1_gas_used,
230 l1_fee_scalar,
231 l1_base_fee: l1_gas_price,
232 deposit_nonce,
233 deposit_receipt_version,
234 l1_base_fee_scalar,
235 l1_blob_base_fee,
236 l1_blob_base_fee_scalar,
237 operator_fee_scalar,
238 operator_fee_constant,
239 } = self;
240
241 OpTransactionReceiptFields {
242 l1_block_info: L1BlockInfo {
243 l1_gas_price,
244 l1_gas_used,
245 l1_fee,
246 l1_fee_scalar,
247 l1_base_fee_scalar,
248 l1_blob_base_fee,
249 l1_blob_base_fee_scalar,
250 operator_fee_scalar,
251 operator_fee_constant,
252 },
253 deposit_nonce,
254 deposit_receipt_version,
255 }
256 }
257}
258
259#[derive(Debug)]
261pub struct OpReceiptBuilder {
262 pub core_receipt: TransactionReceipt<OpReceiptEnvelope<Log>>,
264 pub op_receipt_fields: OpTransactionReceiptFields,
266}
267
268impl OpReceiptBuilder {
269 pub fn new<N>(
271 chain_spec: &impl OpHardforks,
272 input: ConvertReceiptInput<'_, N>,
273 l1_block_info: &mut op_revm::L1BlockInfo,
274 ) -> Result<Self, OpEthApiError>
275 where
276 N: NodePrimitives<SignedTx: OpTransaction, Receipt = OpReceipt>,
277 {
278 let timestamp = input.meta.timestamp;
279 let block_number = input.meta.block_number;
280 let tx_signed = *input.tx.inner();
281 let core_receipt = build_receipt(input, None, |receipt, next_log_index, meta| {
282 let map_logs = move |receipt: alloy_consensus::Receipt| {
283 let Receipt { status, cumulative_gas_used, logs } = receipt;
284 let logs = Log::collect_for_receipt(next_log_index, meta, logs);
285 Receipt { status, cumulative_gas_used, logs }
286 };
287 match receipt {
288 OpReceipt::Legacy(receipt) => {
289 OpReceiptEnvelope::Legacy(map_logs(receipt).into_with_bloom())
290 }
291 OpReceipt::Eip2930(receipt) => {
292 OpReceiptEnvelope::Eip2930(map_logs(receipt).into_with_bloom())
293 }
294 OpReceipt::Eip1559(receipt) => {
295 OpReceiptEnvelope::Eip1559(map_logs(receipt).into_with_bloom())
296 }
297 OpReceipt::Eip7702(receipt) => {
298 OpReceiptEnvelope::Eip7702(map_logs(receipt).into_with_bloom())
299 }
300 OpReceipt::Deposit(receipt) => {
301 OpReceiptEnvelope::Deposit(receipt.map_inner(map_logs).into_with_bloom())
302 }
303 }
304 });
305
306 let op_receipt_fields = OpReceiptFieldsBuilder::new(timestamp, block_number)
307 .l1_block_info(chain_spec, tx_signed, l1_block_info)?
308 .build();
309
310 Ok(Self { core_receipt, op_receipt_fields })
311 }
312
313 pub fn build(self) -> OpTransactionReceipt {
316 let Self { core_receipt: inner, op_receipt_fields } = self;
317
318 let OpTransactionReceiptFields { l1_block_info, .. } = op_receipt_fields;
319
320 OpTransactionReceipt { inner, l1_block_info }
321 }
322}
323
324#[cfg(test)]
325mod test {
326 use super::*;
327 use alloy_consensus::{Block, BlockBody};
328 use alloy_primitives::{hex, U256};
329 use op_alloy_network::eip2718::Decodable2718;
330 use reth_optimism_chainspec::{BASE_MAINNET, OP_MAINNET};
331 use reth_optimism_primitives::OpTransactionSigned;
332
333 const TX_SET_L1_BLOCK_OP_MAINNET_BLOCK_124665056: [u8; 251] = hex!(
337 "7ef8f8a0683079df94aa5b9cf86687d739a60a9b4f0835e520ec4d664e2e415dca17a6df94deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e200000146b000f79c500000000000000040000000066d052e700000000013ad8a3000000000000000000000000000000000000000000000000000000003ef1278700000000000000000000000000000000000000000000000000000000000000012fdf87b89884a61e74b322bbcf60386f543bfae7827725efaaf0ab1de2294a590000000000000000000000006887246668a3b87f54deb3b94ba47a6f63f32985"
338 );
339
340 const TX_1_OP_MAINNET_BLOCK_124665056: [u8; 1176] = hex!(
344 "02f904940a8303fba78401d6d2798401db2b6d830493e0943e6f4f7866654c18f536170780344aa8772950b680b904246a761202000000000000000000000000087000a300de7200382b55d40045000000e5d60e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003a0000000000000000000000000000000000000000000000000000000000000022482ad56cb0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000120000000000000000000000000dc6ff44d5d932cbd77b52e5612ba0529dc6226f1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000021c4928109acb0659a88ae5329b5374a3024694c0000000000000000000000000000000000000000000000049b9ca9a6943400000000000000000000000000000000000000000000000000000000000000000000000000000000000021c4928109acb0659a88ae5329b5374a3024694c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000024b6b55f250000000000000000000000000000000000000000000000049b9ca9a694340000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000415ec214a3950bea839a7e6fbb0ba1540ac2076acd50820e2d5ef83d0902cdffb24a47aff7de5190290769c4f0a9c6fabf63012986a0d590b1b571547a8c7050ea1b00000000000000000000000000000000000000000000000000000000000000c080a06db770e6e25a617fe9652f0958bd9bd6e49281a53036906386ed39ec48eadf63a07f47cf51a4a40b4494cf26efc686709a9b03939e20ee27e59682f5faa536667e"
345 );
346
347 const BLOCK_124665056_TIMESTAMP: u64 = 1724928889;
351
352 const TX_META_TX_1_OP_MAINNET_BLOCK_124665056: OpTransactionReceiptFields =
356 OpTransactionReceiptFields {
357 l1_block_info: L1BlockInfo {
358 l1_gas_price: Some(1055991687), l1_gas_used: Some(4471),
360 l1_fee: Some(24681034813),
361 l1_fee_scalar: None,
362 l1_base_fee_scalar: Some(5227),
363 l1_blob_base_fee: Some(1),
364 l1_blob_base_fee_scalar: Some(1014213),
365 operator_fee_scalar: None,
366 operator_fee_constant: None,
367 },
368 deposit_nonce: None,
369 deposit_receipt_version: None,
370 };
371
372 #[test]
373 fn op_receipt_fields_from_block_and_tx() {
374 let tx_0 = OpTransactionSigned::decode_2718(
376 &mut TX_SET_L1_BLOCK_OP_MAINNET_BLOCK_124665056.as_slice(),
377 )
378 .unwrap();
379
380 let tx_1 =
381 OpTransactionSigned::decode_2718(&mut TX_1_OP_MAINNET_BLOCK_124665056.as_slice())
382 .unwrap();
383
384 let block: Block<OpTransactionSigned> = Block {
385 body: BlockBody { transactions: [tx_0, tx_1.clone()].to_vec(), ..Default::default() },
386 ..Default::default()
387 };
388
389 let mut l1_block_info =
390 reth_optimism_evm::extract_l1_info(&block.body).expect("should extract l1 info");
391
392 assert!(OP_MAINNET.is_fjord_active_at_timestamp(BLOCK_124665056_TIMESTAMP));
394
395 let receipt_meta = OpReceiptFieldsBuilder::new(BLOCK_124665056_TIMESTAMP, 124665056)
396 .l1_block_info(&*OP_MAINNET, &tx_1, &mut l1_block_info)
397 .expect("should parse revm l1 info")
398 .build();
399
400 let L1BlockInfo {
401 l1_gas_price,
402 l1_gas_used,
403 l1_fee,
404 l1_fee_scalar,
405 l1_base_fee_scalar,
406 l1_blob_base_fee,
407 l1_blob_base_fee_scalar,
408 operator_fee_scalar,
409 operator_fee_constant,
410 } = receipt_meta.l1_block_info;
411
412 assert_eq!(
413 l1_gas_price, TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_gas_price,
414 "incorrect l1 base fee (former gas price)"
415 );
416 assert_eq!(
417 l1_gas_used, TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_gas_used,
418 "incorrect l1 gas used"
419 );
420 assert_eq!(
421 l1_fee, TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_fee,
422 "incorrect l1 fee"
423 );
424 assert_eq!(
425 l1_fee_scalar, TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_fee_scalar,
426 "incorrect l1 fee scalar"
427 );
428 assert_eq!(
429 l1_base_fee_scalar,
430 TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_base_fee_scalar,
431 "incorrect l1 base fee scalar"
432 );
433 assert_eq!(
434 l1_blob_base_fee,
435 TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_blob_base_fee,
436 "incorrect l1 blob base fee"
437 );
438 assert_eq!(
439 l1_blob_base_fee_scalar,
440 TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_blob_base_fee_scalar,
441 "incorrect l1 blob base fee scalar"
442 );
443 assert_eq!(
444 operator_fee_scalar,
445 TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.operator_fee_scalar,
446 "incorrect operator fee scalar"
447 );
448 assert_eq!(
449 operator_fee_constant,
450 TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.operator_fee_constant,
451 "incorrect operator fee constant"
452 );
453 }
454
455 #[test]
456 fn op_non_zero_operator_fee_params_included_in_receipt() {
457 let tx_1 =
458 OpTransactionSigned::decode_2718(&mut TX_1_OP_MAINNET_BLOCK_124665056.as_slice())
459 .unwrap();
460
461 let mut l1_block_info = op_revm::L1BlockInfo::default();
462
463 l1_block_info.operator_fee_scalar = Some(U256::ZERO);
464 l1_block_info.operator_fee_constant = Some(U256::from(2));
465
466 let receipt_meta = OpReceiptFieldsBuilder::new(BLOCK_124665056_TIMESTAMP, 124665056)
467 .l1_block_info(&*OP_MAINNET, &tx_1, &mut l1_block_info)
468 .expect("should parse revm l1 info")
469 .build();
470
471 let L1BlockInfo { operator_fee_scalar, operator_fee_constant, .. } =
472 receipt_meta.l1_block_info;
473
474 assert_eq!(operator_fee_scalar, Some(0), "incorrect operator fee scalar");
475 assert_eq!(operator_fee_constant, Some(2), "incorrect operator fee constant");
476 }
477
478 #[test]
479 fn op_zero_operator_fee_params_not_included_in_receipt() {
480 let tx_1 =
481 OpTransactionSigned::decode_2718(&mut TX_1_OP_MAINNET_BLOCK_124665056.as_slice())
482 .unwrap();
483
484 let mut l1_block_info = op_revm::L1BlockInfo::default();
485
486 l1_block_info.operator_fee_scalar = Some(U256::ZERO);
487 l1_block_info.operator_fee_constant = Some(U256::ZERO);
488
489 let receipt_meta = OpReceiptFieldsBuilder::new(BLOCK_124665056_TIMESTAMP, 124665056)
490 .l1_block_info(&*OP_MAINNET, &tx_1, &mut l1_block_info)
491 .expect("should parse revm l1 info")
492 .build();
493
494 let L1BlockInfo { operator_fee_scalar, operator_fee_constant, .. } =
495 receipt_meta.l1_block_info;
496
497 assert_eq!(operator_fee_scalar, None, "incorrect operator fee scalar");
498 assert_eq!(operator_fee_constant, None, "incorrect operator fee constant");
499 }
500
501 #[test]
503 fn base_receipt_gas_fields() {
504 let system = hex!(
506 "7ef8f8a0389e292420bcbf9330741f72074e39562a09ff5a00fd22e4e9eee7e34b81bca494deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e20000008dd00101c120000000000000004000000006721035b00000000014189960000000000000000000000000000000000000000000000000000000349b4dcdc000000000000000000000000000000000000000000000000000000004ef9325cc5991ce750960f636ca2ffbb6e209bb3ba91412f21dd78c14ff154d1930f1f9a0000000000000000000000005050f69a9786f081509234f1a7f4684b5e5b76c9"
507 );
508 let tx_0 = OpTransactionSigned::decode_2718(&mut &system[..]).unwrap();
509
510 let block: alloy_consensus::Block<OpTransactionSigned> = Block {
511 body: BlockBody { transactions: vec![tx_0], ..Default::default() },
512 ..Default::default()
513 };
514 let mut l1_block_info =
515 reth_optimism_evm::extract_l1_info(&block.body).expect("should extract l1 info");
516
517 let tx = hex!(
519 "02f86c8221058034839a4ae283021528942f16386bb37709016023232523ff6d9daf444be380841249c58bc080a001b927eda2af9b00b52a57be0885e0303c39dd2831732e14051c2336470fd468a0681bf120baf562915841a48601c2b54a6742511e535cf8f71c95115af7ff63bd"
520 );
521 let tx_1 = OpTransactionSigned::decode_2718(&mut &tx[..]).unwrap();
522
523 let receipt_meta = OpReceiptFieldsBuilder::new(1730216981, 21713817)
524 .l1_block_info(&*BASE_MAINNET, &tx_1, &mut l1_block_info)
525 .expect("should parse revm l1 info")
526 .build();
527
528 let L1BlockInfo {
529 l1_gas_price,
530 l1_gas_used,
531 l1_fee,
532 l1_fee_scalar,
533 l1_base_fee_scalar,
534 l1_blob_base_fee,
535 l1_blob_base_fee_scalar,
536 operator_fee_scalar,
537 operator_fee_constant,
538 } = receipt_meta.l1_block_info;
539
540 assert_eq!(l1_gas_price, Some(14121491676), "incorrect l1 base fee (former gas price)");
541 assert_eq!(l1_gas_used, Some(1600), "incorrect l1 gas used");
542 assert_eq!(l1_fee, Some(191150293412), "incorrect l1 fee");
543 assert!(l1_fee_scalar.is_none(), "incorrect l1 fee scalar");
544 assert_eq!(l1_base_fee_scalar, Some(2269), "incorrect l1 base fee scalar");
545 assert_eq!(l1_blob_base_fee, Some(1324954204), "incorrect l1 blob base fee");
546 assert_eq!(l1_blob_base_fee_scalar, Some(1055762), "incorrect l1 blob base fee scalar");
547 assert_eq!(operator_fee_scalar, None, "incorrect operator fee scalar");
548 assert_eq!(operator_fee_constant, None, "incorrect operator fee constant");
549 }
550}