1use crate::{eth::RpcNodeCore, OpEthApi, OpEthApiError};
4use alloy_eips::eip2718::Encodable2718;
5use alloy_rpc_types_eth::{Log, TransactionReceipt};
6use op_alloy_consensus::{
7 OpDepositReceipt, OpDepositReceiptWithBloom, OpReceiptEnvelope, OpTransaction,
8};
9use op_alloy_rpc_types::{L1BlockInfo, OpTransactionReceipt, OpTransactionReceiptFields};
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::Block;
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#[derive(Debug, Clone)]
34pub struct OpReceiptConverter<Provider> {
35 provider: Provider,
36}
37
38impl<Provider> OpReceiptConverter<Provider> {
39 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: BlockReader + 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 let mut l1_block_info = match reth_optimism_evm::extract_l1_info(block.body()) {
67 Ok(l1_block_info) => l1_block_info,
68 Err(err) => {
69 if block_number == 0 {
72 return Ok(vec![]);
73 }
74 return Err(err.into());
75 }
76 };
77
78 let mut receipts = Vec::with_capacity(inputs.len());
79
80 for input in inputs {
81 l1_block_info.clear_tx_l1_cost();
85
86 receipts.push(
87 OpReceiptBuilder::new(&self.provider.chain_spec(), input, &mut l1_block_info)?
88 .build(),
89 );
90 }
91
92 Ok(receipts)
93 }
94}
95
96#[derive(Debug, Clone)]
99pub struct OpReceiptFieldsBuilder {
100 pub block_number: u64,
102 pub block_timestamp: u64,
104 pub l1_fee: Option<u128>,
106 pub l1_data_gas: Option<u128>,
108 pub l1_fee_scalar: Option<f64>,
110 pub l1_base_fee: Option<u128>,
113 pub deposit_nonce: Option<u64>,
116 pub deposit_receipt_version: Option<u64>,
119 pub l1_base_fee_scalar: Option<u128>,
122 pub l1_blob_base_fee: Option<u128>,
124 pub l1_blob_base_fee_scalar: Option<u128>,
126 pub operator_fee_scalar: Option<u128>,
128 pub operator_fee_constant: Option<u128>,
130}
131
132impl OpReceiptFieldsBuilder {
133 pub const fn new(block_timestamp: u64, block_number: u64) -> Self {
135 Self {
136 block_number,
137 block_timestamp,
138 l1_fee: None,
139 l1_data_gas: None,
140 l1_fee_scalar: None,
141 l1_base_fee: None,
142 deposit_nonce: None,
143 deposit_receipt_version: None,
144 l1_base_fee_scalar: None,
145 l1_blob_base_fee: None,
146 l1_blob_base_fee_scalar: None,
147 operator_fee_scalar: None,
148 operator_fee_constant: None,
149 }
150 }
151
152 pub fn l1_block_info<T: Encodable2718 + OpTransaction>(
154 mut self,
155 chain_spec: &impl OpHardforks,
156 tx: &T,
157 l1_block_info: &mut op_revm::L1BlockInfo,
158 ) -> Result<Self, OpEthApiError> {
159 let raw_tx = tx.encoded_2718();
160 let timestamp = self.block_timestamp;
161
162 self.l1_fee = Some(
163 l1_block_info
164 .l1_tx_data_fee(chain_spec, timestamp, &raw_tx, tx.is_deposit())
165 .map_err(|_| OpEthApiError::L1BlockFeeError)?
166 .saturating_to(),
167 );
168
169 self.l1_data_gas = Some(
170 l1_block_info
171 .l1_data_gas(chain_spec, timestamp, &raw_tx)
172 .map_err(|_| OpEthApiError::L1BlockGasError)?
173 .saturating_add(l1_block_info.l1_fee_overhead.unwrap_or_default())
174 .saturating_to(),
175 );
176
177 self.l1_fee_scalar = (!chain_spec.is_ecotone_active_at_timestamp(timestamp))
178 .then_some(f64::from(l1_block_info.l1_base_fee_scalar) / 1_000_000.0);
179
180 self.l1_base_fee = Some(l1_block_info.l1_base_fee.saturating_to());
181 self.l1_base_fee_scalar = Some(l1_block_info.l1_base_fee_scalar.saturating_to());
182 self.l1_blob_base_fee = l1_block_info.l1_blob_base_fee.map(|fee| fee.saturating_to());
183 self.l1_blob_base_fee_scalar =
184 l1_block_info.l1_blob_base_fee_scalar.map(|scalar| scalar.saturating_to());
185
186 let operator_fee_scalar_has_non_zero_value: bool =
188 l1_block_info.operator_fee_scalar.is_some_and(|scalar| !scalar.is_zero());
189
190 let operator_fee_constant_has_non_zero_value =
191 l1_block_info.operator_fee_constant.is_some_and(|constant| !constant.is_zero());
192
193 if operator_fee_scalar_has_non_zero_value || operator_fee_constant_has_non_zero_value {
194 self.operator_fee_scalar =
195 l1_block_info.operator_fee_scalar.map(|scalar| scalar.saturating_to());
196 self.operator_fee_constant =
197 l1_block_info.operator_fee_constant.map(|constant| constant.saturating_to());
198 }
199
200 Ok(self)
201 }
202
203 pub const fn deposit_nonce(mut self, nonce: Option<u64>) -> Self {
205 self.deposit_nonce = nonce;
206 self
207 }
208
209 pub const fn deposit_version(mut self, version: Option<u64>) -> Self {
211 self.deposit_receipt_version = version;
212 self
213 }
214
215 pub const fn build(self) -> OpTransactionReceiptFields {
217 let Self {
218 block_number: _, block_timestamp: _, l1_fee,
221 l1_data_gas: l1_gas_used,
222 l1_fee_scalar,
223 l1_base_fee: l1_gas_price,
224 deposit_nonce,
225 deposit_receipt_version,
226 l1_base_fee_scalar,
227 l1_blob_base_fee,
228 l1_blob_base_fee_scalar,
229 operator_fee_scalar,
230 operator_fee_constant,
231 } = self;
232
233 OpTransactionReceiptFields {
234 l1_block_info: L1BlockInfo {
235 l1_gas_price,
236 l1_gas_used,
237 l1_fee,
238 l1_fee_scalar,
239 l1_base_fee_scalar,
240 l1_blob_base_fee,
241 l1_blob_base_fee_scalar,
242 operator_fee_scalar,
243 operator_fee_constant,
244 },
245 deposit_nonce,
246 deposit_receipt_version,
247 }
248 }
249}
250
251#[derive(Debug)]
253pub struct OpReceiptBuilder {
254 pub core_receipt: TransactionReceipt<OpReceiptEnvelope<Log>>,
256 pub op_receipt_fields: OpTransactionReceiptFields,
258}
259
260impl OpReceiptBuilder {
261 pub fn new<N>(
263 chain_spec: &impl OpHardforks,
264 input: ConvertReceiptInput<'_, N>,
265 l1_block_info: &mut op_revm::L1BlockInfo,
266 ) -> Result<Self, OpEthApiError>
267 where
268 N: NodePrimitives<SignedTx: OpTransaction, Receipt = OpReceipt>,
269 {
270 let timestamp = input.meta.timestamp;
271 let block_number = input.meta.block_number;
272 let tx_signed = *input.tx.inner();
273 let core_receipt =
274 build_receipt(&input, None, |receipt_with_bloom| match input.receipt.as_ref() {
275 OpReceipt::Legacy(_) => OpReceiptEnvelope::Legacy(receipt_with_bloom),
276 OpReceipt::Eip2930(_) => OpReceiptEnvelope::Eip2930(receipt_with_bloom),
277 OpReceipt::Eip1559(_) => OpReceiptEnvelope::Eip1559(receipt_with_bloom),
278 OpReceipt::Eip7702(_) => OpReceiptEnvelope::Eip7702(receipt_with_bloom),
279 OpReceipt::Deposit(receipt) => {
280 OpReceiptEnvelope::Deposit(OpDepositReceiptWithBloom {
281 receipt: OpDepositReceipt {
282 inner: receipt_with_bloom.receipt,
283 deposit_nonce: receipt.deposit_nonce,
284 deposit_receipt_version: receipt.deposit_receipt_version,
285 },
286 logs_bloom: receipt_with_bloom.logs_bloom,
287 })
288 }
289 });
290
291 let op_receipt_fields = OpReceiptFieldsBuilder::new(timestamp, block_number)
292 .l1_block_info(chain_spec, tx_signed, l1_block_info)?
293 .build();
294
295 Ok(Self { core_receipt, op_receipt_fields })
296 }
297
298 pub fn build(self) -> OpTransactionReceipt {
301 let Self { core_receipt: inner, op_receipt_fields } = self;
302
303 let OpTransactionReceiptFields { l1_block_info, .. } = op_receipt_fields;
304
305 OpTransactionReceipt { inner, l1_block_info }
306 }
307}
308
309#[cfg(test)]
310mod test {
311 use super::*;
312 use alloy_consensus::{Block, BlockBody};
313 use alloy_primitives::{hex, U256};
314 use op_alloy_network::eip2718::Decodable2718;
315 use reth_optimism_chainspec::{BASE_MAINNET, OP_MAINNET};
316 use reth_optimism_primitives::OpTransactionSigned;
317
318 const TX_SET_L1_BLOCK_OP_MAINNET_BLOCK_124665056: [u8; 251] = hex!(
322 "7ef8f8a0683079df94aa5b9cf86687d739a60a9b4f0835e520ec4d664e2e415dca17a6df94deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e200000146b000f79c500000000000000040000000066d052e700000000013ad8a3000000000000000000000000000000000000000000000000000000003ef1278700000000000000000000000000000000000000000000000000000000000000012fdf87b89884a61e74b322bbcf60386f543bfae7827725efaaf0ab1de2294a590000000000000000000000006887246668a3b87f54deb3b94ba47a6f63f32985"
323 );
324
325 const TX_1_OP_MAINNET_BLOCK_124665056: [u8; 1176] = hex!(
329 "02f904940a8303fba78401d6d2798401db2b6d830493e0943e6f4f7866654c18f536170780344aa8772950b680b904246a761202000000000000000000000000087000a300de7200382b55d40045000000e5d60e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003a0000000000000000000000000000000000000000000000000000000000000022482ad56cb0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000120000000000000000000000000dc6ff44d5d932cbd77b52e5612ba0529dc6226f1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000021c4928109acb0659a88ae5329b5374a3024694c0000000000000000000000000000000000000000000000049b9ca9a6943400000000000000000000000000000000000000000000000000000000000000000000000000000000000021c4928109acb0659a88ae5329b5374a3024694c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000024b6b55f250000000000000000000000000000000000000000000000049b9ca9a694340000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000415ec214a3950bea839a7e6fbb0ba1540ac2076acd50820e2d5ef83d0902cdffb24a47aff7de5190290769c4f0a9c6fabf63012986a0d590b1b571547a8c7050ea1b00000000000000000000000000000000000000000000000000000000000000c080a06db770e6e25a617fe9652f0958bd9bd6e49281a53036906386ed39ec48eadf63a07f47cf51a4a40b4494cf26efc686709a9b03939e20ee27e59682f5faa536667e"
330 );
331
332 const BLOCK_124665056_TIMESTAMP: u64 = 1724928889;
336
337 const TX_META_TX_1_OP_MAINNET_BLOCK_124665056: OpTransactionReceiptFields =
341 OpTransactionReceiptFields {
342 l1_block_info: L1BlockInfo {
343 l1_gas_price: Some(1055991687), l1_gas_used: Some(4471),
345 l1_fee: Some(24681034813),
346 l1_fee_scalar: None,
347 l1_base_fee_scalar: Some(5227),
348 l1_blob_base_fee: Some(1),
349 l1_blob_base_fee_scalar: Some(1014213),
350 operator_fee_scalar: None,
351 operator_fee_constant: None,
352 },
353 deposit_nonce: None,
354 deposit_receipt_version: None,
355 };
356
357 #[test]
358 fn op_receipt_fields_from_block_and_tx() {
359 let tx_0 = OpTransactionSigned::decode_2718(
361 &mut TX_SET_L1_BLOCK_OP_MAINNET_BLOCK_124665056.as_slice(),
362 )
363 .unwrap();
364
365 let tx_1 =
366 OpTransactionSigned::decode_2718(&mut TX_1_OP_MAINNET_BLOCK_124665056.as_slice())
367 .unwrap();
368
369 let block: Block<OpTransactionSigned> = Block {
370 body: BlockBody { transactions: [tx_0, tx_1.clone()].to_vec(), ..Default::default() },
371 ..Default::default()
372 };
373
374 let mut l1_block_info =
375 reth_optimism_evm::extract_l1_info(&block.body).expect("should extract l1 info");
376
377 assert!(OP_MAINNET.is_fjord_active_at_timestamp(BLOCK_124665056_TIMESTAMP));
379
380 let receipt_meta = OpReceiptFieldsBuilder::new(BLOCK_124665056_TIMESTAMP, 124665056)
381 .l1_block_info(&*OP_MAINNET, &tx_1, &mut l1_block_info)
382 .expect("should parse revm l1 info")
383 .build();
384
385 let L1BlockInfo {
386 l1_gas_price,
387 l1_gas_used,
388 l1_fee,
389 l1_fee_scalar,
390 l1_base_fee_scalar,
391 l1_blob_base_fee,
392 l1_blob_base_fee_scalar,
393 operator_fee_scalar,
394 operator_fee_constant,
395 } = receipt_meta.l1_block_info;
396
397 assert_eq!(
398 l1_gas_price, TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_gas_price,
399 "incorrect l1 base fee (former gas price)"
400 );
401 assert_eq!(
402 l1_gas_used, TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_gas_used,
403 "incorrect l1 gas used"
404 );
405 assert_eq!(
406 l1_fee, TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_fee,
407 "incorrect l1 fee"
408 );
409 assert_eq!(
410 l1_fee_scalar, TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_fee_scalar,
411 "incorrect l1 fee scalar"
412 );
413 assert_eq!(
414 l1_base_fee_scalar,
415 TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_base_fee_scalar,
416 "incorrect l1 base fee scalar"
417 );
418 assert_eq!(
419 l1_blob_base_fee,
420 TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_blob_base_fee,
421 "incorrect l1 blob base fee"
422 );
423 assert_eq!(
424 l1_blob_base_fee_scalar,
425 TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_blob_base_fee_scalar,
426 "incorrect l1 blob base fee scalar"
427 );
428 assert_eq!(
429 operator_fee_scalar,
430 TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.operator_fee_scalar,
431 "incorrect operator fee scalar"
432 );
433 assert_eq!(
434 operator_fee_constant,
435 TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.operator_fee_constant,
436 "incorrect operator fee constant"
437 );
438 }
439
440 #[test]
441 fn op_non_zero_operator_fee_params_included_in_receipt() {
442 let tx_1 =
443 OpTransactionSigned::decode_2718(&mut TX_1_OP_MAINNET_BLOCK_124665056.as_slice())
444 .unwrap();
445
446 let mut l1_block_info = op_revm::L1BlockInfo::default();
447
448 l1_block_info.operator_fee_scalar = Some(U256::ZERO);
449 l1_block_info.operator_fee_constant = Some(U256::from(2));
450
451 let receipt_meta = OpReceiptFieldsBuilder::new(BLOCK_124665056_TIMESTAMP, 124665056)
452 .l1_block_info(&*OP_MAINNET, &tx_1, &mut l1_block_info)
453 .expect("should parse revm l1 info")
454 .build();
455
456 let L1BlockInfo { operator_fee_scalar, operator_fee_constant, .. } =
457 receipt_meta.l1_block_info;
458
459 assert_eq!(operator_fee_scalar, Some(0), "incorrect operator fee scalar");
460 assert_eq!(operator_fee_constant, Some(2), "incorrect operator fee constant");
461 }
462
463 #[test]
464 fn op_zero_operator_fee_params_not_included_in_receipt() {
465 let tx_1 =
466 OpTransactionSigned::decode_2718(&mut TX_1_OP_MAINNET_BLOCK_124665056.as_slice())
467 .unwrap();
468
469 let mut l1_block_info = op_revm::L1BlockInfo::default();
470
471 l1_block_info.operator_fee_scalar = Some(U256::ZERO);
472 l1_block_info.operator_fee_constant = Some(U256::ZERO);
473
474 let receipt_meta = OpReceiptFieldsBuilder::new(BLOCK_124665056_TIMESTAMP, 124665056)
475 .l1_block_info(&*OP_MAINNET, &tx_1, &mut l1_block_info)
476 .expect("should parse revm l1 info")
477 .build();
478
479 let L1BlockInfo { operator_fee_scalar, operator_fee_constant, .. } =
480 receipt_meta.l1_block_info;
481
482 assert_eq!(operator_fee_scalar, None, "incorrect operator fee scalar");
483 assert_eq!(operator_fee_constant, None, "incorrect operator fee constant");
484 }
485
486 #[test]
488 fn base_receipt_gas_fields() {
489 let system = hex!(
491 "7ef8f8a0389e292420bcbf9330741f72074e39562a09ff5a00fd22e4e9eee7e34b81bca494deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e20000008dd00101c120000000000000004000000006721035b00000000014189960000000000000000000000000000000000000000000000000000000349b4dcdc000000000000000000000000000000000000000000000000000000004ef9325cc5991ce750960f636ca2ffbb6e209bb3ba91412f21dd78c14ff154d1930f1f9a0000000000000000000000005050f69a9786f081509234f1a7f4684b5e5b76c9"
492 );
493 let tx_0 = OpTransactionSigned::decode_2718(&mut &system[..]).unwrap();
494
495 let block: alloy_consensus::Block<OpTransactionSigned> = Block {
496 body: BlockBody { transactions: vec![tx_0], ..Default::default() },
497 ..Default::default()
498 };
499 let mut l1_block_info =
500 reth_optimism_evm::extract_l1_info(&block.body).expect("should extract l1 info");
501
502 let tx = hex!(
504 "02f86c8221058034839a4ae283021528942f16386bb37709016023232523ff6d9daf444be380841249c58bc080a001b927eda2af9b00b52a57be0885e0303c39dd2831732e14051c2336470fd468a0681bf120baf562915841a48601c2b54a6742511e535cf8f71c95115af7ff63bd"
505 );
506 let tx_1 = OpTransactionSigned::decode_2718(&mut &tx[..]).unwrap();
507
508 let receipt_meta = OpReceiptFieldsBuilder::new(1730216981, 21713817)
509 .l1_block_info(&*BASE_MAINNET, &tx_1, &mut l1_block_info)
510 .expect("should parse revm l1 info")
511 .build();
512
513 let L1BlockInfo {
514 l1_gas_price,
515 l1_gas_used,
516 l1_fee,
517 l1_fee_scalar,
518 l1_base_fee_scalar,
519 l1_blob_base_fee,
520 l1_blob_base_fee_scalar,
521 operator_fee_scalar,
522 operator_fee_constant,
523 } = receipt_meta.l1_block_info;
524
525 assert_eq!(l1_gas_price, Some(14121491676), "incorrect l1 base fee (former gas price)");
526 assert_eq!(l1_gas_used, Some(1600), "incorrect l1 gas used");
527 assert_eq!(l1_fee, Some(191150293412), "incorrect l1 fee");
528 assert!(l1_fee_scalar.is_none(), "incorrect l1 fee scalar");
529 assert_eq!(l1_base_fee_scalar, Some(2269), "incorrect l1 base fee scalar");
530 assert_eq!(l1_blob_base_fee, Some(1324954204), "incorrect l1 blob base fee");
531 assert_eq!(l1_blob_base_fee_scalar, Some(1055762), "incorrect l1 blob base fee scalar");
532 assert_eq!(operator_fee_scalar, None, "incorrect operator fee scalar");
533 assert_eq!(operator_fee_constant, None, "incorrect operator fee constant");
534 }
535}