use alloy_eips::eip2718::Encodable2718;
use alloy_rpc_types_eth::{Log, TransactionReceipt};
use op_alloy_consensus::{
DepositTransaction, OpDepositReceipt, OpDepositReceiptWithBloom, OpReceiptEnvelope,
};
use op_alloy_rpc_types::{L1BlockInfo, OpTransactionReceipt, OpTransactionReceiptFields};
use reth_node_api::{FullNodeComponents, NodeTypes};
use reth_optimism_chainspec::OpChainSpec;
use reth_optimism_evm::RethL1BlockInfo;
use reth_optimism_forks::OpHardforks;
use reth_primitives::{Receipt, TransactionMeta, TransactionSigned, TxType};
use reth_provider::{ChainSpecProvider, ReceiptProvider, TransactionsProvider};
use reth_rpc_eth_api::{helpers::LoadReceipt, FromEthApiError, RpcReceipt};
use reth_rpc_eth_types::{receipt::build_receipt, EthApiError};
use crate::{OpEthApi, OpEthApiError};
impl<N> LoadReceipt for OpEthApi<N>
where
Self: Send + Sync,
N: FullNodeComponents<Types: NodeTypes<ChainSpec = OpChainSpec>>,
Self::Provider:
TransactionsProvider<Transaction = TransactionSigned> + ReceiptProvider<Receipt = Receipt>,
{
async fn build_transaction_receipt(
&self,
tx: TransactionSigned,
meta: TransactionMeta,
receipt: Receipt,
) -> Result<RpcReceipt<Self::NetworkTypes>, Self::Error> {
let (block, receipts) = self
.inner
.eth_api
.cache()
.get_block_and_receipts(meta.block_hash)
.await
.map_err(Self::Error::from_eth_err)?
.ok_or(Self::Error::from_eth_err(EthApiError::HeaderNotFound(
meta.block_hash.into(),
)))?;
let l1_block_info =
reth_optimism_evm::extract_l1_info(&block.body).map_err(OpEthApiError::from)?;
Ok(OpReceiptBuilder::new(
&self.inner.eth_api.provider().chain_spec(),
&tx,
meta,
&receipt,
&receipts,
l1_block_info,
)?
.build())
}
}
#[derive(Debug, Clone)]
pub struct OpReceiptFieldsBuilder {
pub block_timestamp: u64,
pub l1_fee: Option<u128>,
pub l1_data_gas: Option<u128>,
pub l1_fee_scalar: Option<f64>,
pub l1_base_fee: Option<u128>,
pub deposit_nonce: Option<u64>,
pub deposit_receipt_version: Option<u64>,
pub l1_base_fee_scalar: Option<u128>,
pub l1_blob_base_fee: Option<u128>,
pub l1_blob_base_fee_scalar: Option<u128>,
}
impl OpReceiptFieldsBuilder {
pub const fn new(block_timestamp: u64) -> Self {
Self {
block_timestamp,
l1_fee: None,
l1_data_gas: None,
l1_fee_scalar: None,
l1_base_fee: None,
deposit_nonce: None,
deposit_receipt_version: None,
l1_base_fee_scalar: None,
l1_blob_base_fee: None,
l1_blob_base_fee_scalar: None,
}
}
pub fn l1_block_info(
mut self,
chain_spec: &OpChainSpec,
tx: &TransactionSigned,
l1_block_info: revm::L1BlockInfo,
) -> Result<Self, OpEthApiError> {
let raw_tx = tx.encoded_2718();
let timestamp = self.block_timestamp;
self.l1_fee = Some(
l1_block_info
.l1_tx_data_fee(chain_spec, timestamp, &raw_tx, tx.is_deposit())
.map_err(|_| OpEthApiError::L1BlockFeeError)?
.saturating_to(),
);
self.l1_data_gas = Some(
l1_block_info
.l1_data_gas(chain_spec, timestamp, &raw_tx)
.map_err(|_| OpEthApiError::L1BlockGasError)?
.saturating_add(l1_block_info.l1_fee_overhead.unwrap_or_default())
.saturating_to(),
);
self.l1_fee_scalar = (!chain_spec.is_ecotone_active_at_timestamp(timestamp))
.then_some(f64::from(l1_block_info.l1_base_fee_scalar) / 1_000_000.0);
self.l1_base_fee = Some(l1_block_info.l1_base_fee.saturating_to());
self.l1_base_fee_scalar = Some(l1_block_info.l1_base_fee_scalar.saturating_to());
self.l1_blob_base_fee = l1_block_info.l1_blob_base_fee.map(|fee| fee.saturating_to());
self.l1_blob_base_fee_scalar =
l1_block_info.l1_blob_base_fee_scalar.map(|scalar| scalar.saturating_to());
Ok(self)
}
pub const fn deposit_nonce(mut self, nonce: Option<u64>) -> Self {
self.deposit_nonce = nonce;
self
}
pub const fn deposit_version(mut self, version: Option<u64>) -> Self {
self.deposit_receipt_version = version;
self
}
pub const fn build(self) -> OpTransactionReceiptFields {
let Self {
block_timestamp: _, l1_fee,
l1_data_gas: l1_gas_used,
l1_fee_scalar,
l1_base_fee: l1_gas_price,
deposit_nonce,
deposit_receipt_version,
l1_base_fee_scalar,
l1_blob_base_fee,
l1_blob_base_fee_scalar,
} = self;
OpTransactionReceiptFields {
l1_block_info: L1BlockInfo {
l1_gas_price,
l1_gas_used,
l1_fee,
l1_fee_scalar,
l1_base_fee_scalar,
l1_blob_base_fee,
l1_blob_base_fee_scalar,
},
deposit_nonce,
deposit_receipt_version,
}
}
}
#[derive(Debug)]
pub struct OpReceiptBuilder {
pub core_receipt: TransactionReceipt<OpReceiptEnvelope<Log>>,
pub op_receipt_fields: OpTransactionReceiptFields,
}
impl OpReceiptBuilder {
pub fn new(
chain_spec: &OpChainSpec,
transaction: &TransactionSigned,
meta: TransactionMeta,
receipt: &Receipt,
all_receipts: &[Receipt],
l1_block_info: revm::L1BlockInfo,
) -> Result<Self, OpEthApiError> {
let timestamp = meta.timestamp;
let core_receipt =
build_receipt(transaction, meta, receipt, all_receipts, |receipt_with_bloom| {
match receipt.tx_type {
TxType::Legacy => OpReceiptEnvelope::<Log>::Legacy(receipt_with_bloom),
TxType::Eip2930 => OpReceiptEnvelope::<Log>::Eip2930(receipt_with_bloom),
TxType::Eip1559 => OpReceiptEnvelope::<Log>::Eip1559(receipt_with_bloom),
TxType::Eip4844 => {
OpReceiptEnvelope::<Log>::Eip1559(receipt_with_bloom)
}
TxType::Eip7702 => OpReceiptEnvelope::<Log>::Eip7702(receipt_with_bloom),
TxType::Deposit => {
OpReceiptEnvelope::<Log>::Deposit(OpDepositReceiptWithBloom::<Log> {
receipt: OpDepositReceipt::<Log> {
inner: receipt_with_bloom.receipt,
deposit_nonce: receipt.deposit_nonce,
deposit_receipt_version: receipt.deposit_receipt_version,
},
logs_bloom: receipt_with_bloom.logs_bloom,
})
}
}
})?;
let op_receipt_fields = OpReceiptFieldsBuilder::new(timestamp)
.l1_block_info(chain_spec, transaction, l1_block_info)?
.deposit_nonce(receipt.deposit_nonce)
.deposit_version(receipt.deposit_receipt_version)
.build();
Ok(Self { core_receipt, op_receipt_fields })
}
pub fn build(self) -> OpTransactionReceipt {
let Self { core_receipt: inner, op_receipt_fields } = self;
let OpTransactionReceiptFields { l1_block_info, .. } = op_receipt_fields;
OpTransactionReceipt { inner, l1_block_info }
}
}
#[cfg(test)]
mod test {
use super::*;
use alloy_primitives::hex;
use op_alloy_network::eip2718::Decodable2718;
use reth_optimism_chainspec::{BASE_MAINNET, OP_MAINNET};
use reth_primitives::{Block, BlockBody};
const TX_SET_L1_BLOCK_OP_MAINNET_BLOCK_124665056: [u8; 251] = hex!("7ef8f8a0683079df94aa5b9cf86687d739a60a9b4f0835e520ec4d664e2e415dca17a6df94deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e200000146b000f79c500000000000000040000000066d052e700000000013ad8a3000000000000000000000000000000000000000000000000000000003ef1278700000000000000000000000000000000000000000000000000000000000000012fdf87b89884a61e74b322bbcf60386f543bfae7827725efaaf0ab1de2294a590000000000000000000000006887246668a3b87f54deb3b94ba47a6f63f32985");
const TX_1_OP_MAINNET_BLOCK_124665056: [u8; 1176] = hex!("02f904940a8303fba78401d6d2798401db2b6d830493e0943e6f4f7866654c18f536170780344aa8772950b680b904246a761202000000000000000000000000087000a300de7200382b55d40045000000e5d60e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003a0000000000000000000000000000000000000000000000000000000000000022482ad56cb0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000120000000000000000000000000dc6ff44d5d932cbd77b52e5612ba0529dc6226f1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000021c4928109acb0659a88ae5329b5374a3024694c0000000000000000000000000000000000000000000000049b9ca9a6943400000000000000000000000000000000000000000000000000000000000000000000000000000000000021c4928109acb0659a88ae5329b5374a3024694c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000024b6b55f250000000000000000000000000000000000000000000000049b9ca9a694340000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000415ec214a3950bea839a7e6fbb0ba1540ac2076acd50820e2d5ef83d0902cdffb24a47aff7de5190290769c4f0a9c6fabf63012986a0d590b1b571547a8c7050ea1b00000000000000000000000000000000000000000000000000000000000000c080a06db770e6e25a617fe9652f0958bd9bd6e49281a53036906386ed39ec48eadf63a07f47cf51a4a40b4494cf26efc686709a9b03939e20ee27e59682f5faa536667e");
const BLOCK_124665056_TIMESTAMP: u64 = 1724928889;
const TX_META_TX_1_OP_MAINNET_BLOCK_124665056: OpTransactionReceiptFields =
OpTransactionReceiptFields {
l1_block_info: L1BlockInfo {
l1_gas_price: Some(1055991687), l1_gas_used: Some(4471),
l1_fee: Some(24681034813),
l1_fee_scalar: None,
l1_base_fee_scalar: Some(5227),
l1_blob_base_fee: Some(1),
l1_blob_base_fee_scalar: Some(1014213),
},
deposit_nonce: None,
deposit_receipt_version: None,
};
#[test]
fn op_receipt_fields_from_block_and_tx() {
let tx_0 = TransactionSigned::decode_2718(
&mut TX_SET_L1_BLOCK_OP_MAINNET_BLOCK_124665056.as_slice(),
)
.unwrap();
let tx_1 = TransactionSigned::decode_2718(&mut TX_1_OP_MAINNET_BLOCK_124665056.as_slice())
.unwrap();
let block = Block {
body: BlockBody { transactions: [tx_0, tx_1.clone()].to_vec(), ..Default::default() },
..Default::default()
};
let l1_block_info =
reth_optimism_evm::extract_l1_info(&block.body).expect("should extract l1 info");
assert!(OP_MAINNET.is_fjord_active_at_timestamp(BLOCK_124665056_TIMESTAMP));
let receipt_meta = OpReceiptFieldsBuilder::new(BLOCK_124665056_TIMESTAMP)
.l1_block_info(&OP_MAINNET, &tx_1, l1_block_info)
.expect("should parse revm l1 info")
.build();
let L1BlockInfo {
l1_gas_price,
l1_gas_used,
l1_fee,
l1_fee_scalar,
l1_base_fee_scalar,
l1_blob_base_fee,
l1_blob_base_fee_scalar,
} = receipt_meta.l1_block_info;
assert_eq!(
l1_gas_price, TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_gas_price,
"incorrect l1 base fee (former gas price)"
);
assert_eq!(
l1_gas_used, TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_gas_used,
"incorrect l1 gas used"
);
assert_eq!(
l1_fee, TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_fee,
"incorrect l1 fee"
);
assert_eq!(
l1_fee_scalar, TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_fee_scalar,
"incorrect l1 fee scalar"
);
assert_eq!(
l1_base_fee_scalar,
TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_base_fee_scalar,
"incorrect l1 base fee scalar"
);
assert_eq!(
l1_blob_base_fee,
TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_blob_base_fee,
"incorrect l1 blob base fee"
);
assert_eq!(
l1_blob_base_fee_scalar,
TX_META_TX_1_OP_MAINNET_BLOCK_124665056.l1_block_info.l1_blob_base_fee_scalar,
"incorrect l1 blob base fee scalar"
);
}
#[test]
fn base_receipt_gas_fields() {
let system = hex!("7ef8f8a0389e292420bcbf9330741f72074e39562a09ff5a00fd22e4e9eee7e34b81bca494deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e20000008dd00101c120000000000000004000000006721035b00000000014189960000000000000000000000000000000000000000000000000000000349b4dcdc000000000000000000000000000000000000000000000000000000004ef9325cc5991ce750960f636ca2ffbb6e209bb3ba91412f21dd78c14ff154d1930f1f9a0000000000000000000000005050f69a9786f081509234f1a7f4684b5e5b76c9");
let tx_0 = TransactionSigned::decode_2718(&mut &system[..]).unwrap();
let block = Block {
body: BlockBody { transactions: vec![tx_0], ..Default::default() },
..Default::default()
};
let l1_block_info =
reth_optimism_evm::extract_l1_info(&block.body).expect("should extract l1 info");
let tx = hex!("02f86c8221058034839a4ae283021528942f16386bb37709016023232523ff6d9daf444be380841249c58bc080a001b927eda2af9b00b52a57be0885e0303c39dd2831732e14051c2336470fd468a0681bf120baf562915841a48601c2b54a6742511e535cf8f71c95115af7ff63bd");
let tx_1 = TransactionSigned::decode_2718(&mut &tx[..]).unwrap();
let receipt_meta = OpReceiptFieldsBuilder::new(1730216981)
.l1_block_info(&BASE_MAINNET, &tx_1, l1_block_info)
.expect("should parse revm l1 info")
.build();
let L1BlockInfo {
l1_gas_price,
l1_gas_used,
l1_fee,
l1_fee_scalar,
l1_base_fee_scalar,
l1_blob_base_fee,
l1_blob_base_fee_scalar,
} = receipt_meta.l1_block_info;
assert_eq!(l1_gas_price, Some(14121491676), "incorrect l1 base fee (former gas price)");
assert_eq!(l1_gas_used, Some(1600), "incorrect l1 gas used");
assert_eq!(l1_fee, Some(191150293412), "incorrect l1 fee");
assert!(l1_fee_scalar.is_none(), "incorrect l1 fee scalar");
assert_eq!(l1_base_fee_scalar, Some(2269), "incorrect l1 base fee scalar");
assert_eq!(l1_blob_base_fee, Some(1324954204), "incorrect l1 blob base fee");
assert_eq!(l1_blob_base_fee_scalar, Some(1055762), "incorrect l1 blob base fee scalar");
}
}