//! Database access for `eth_` transaction RPC methods. Loads transaction and receipt data w.r.t. //! network. use std::{fmt, ops::Deref, sync::Arc}; use alloy_dyn_abi::TypedData; use futures::Future; use reth_primitives::{ Address, BlockId, Bytes, Receipt, SealedBlockWithSenders, TransactionMeta, TransactionSigned, TxHash, TxKind, B256, U256, }; use reth_provider::{BlockReaderIdExt, ReceiptProvider, TransactionsProvider}; use reth_rpc_eth_types::{ utils::recover_raw_transaction, EthApiError, EthResult, EthStateCache, SignError, TransactionSource, }; use reth_rpc_types::{ transaction::{ EIP1559TransactionRequest, EIP2930TransactionRequest, EIP4844TransactionRequest, LegacyTransactionRequest, }, AnyTransactionReceipt, Transaction, TransactionRequest, TypedTransactionRequest, }; use reth_rpc_types_compat::transaction::from_recovered_with_block_context; use reth_transaction_pool::{PoolTransaction, TransactionOrigin, TransactionPool}; use crate::{FromEthApiError, IntoEthApiError}; use super::{ Call, EthApiSpec, EthSigner, LoadBlock, LoadFee, LoadPendingBlock, LoadReceipt, SpawnBlocking, }; /// Transaction related functions for the [`EthApiServer`](crate::EthApiServer) trait in /// the `eth_` namespace. /// /// This includes utilities for transaction tracing, transacting and inspection. /// /// Async functions that are spawned onto the /// [`BlockingTaskPool`](reth_tasks::pool::BlockingTaskPool) begin with `spawn_` /// /// ## Calls /// /// There are subtle differences between when transacting [`TransactionRequest`]: /// /// The endpoints `eth_call` and `eth_estimateGas` and `eth_createAccessList` should always /// __disable__ the base fee check in the /// [`EnvWithHandlerCfg`](revm_primitives::CfgEnvWithHandlerCfg). /// /// The behaviour for tracing endpoints is not consistent across clients. /// Geth also disables the basefee check for tracing: /// Erigon does not: /// /// See also /// /// This implementation follows the behaviour of Geth and disables the basefee check for tracing. pub trait EthTransactions: LoadTransaction { /// Returns a handle for reading data from disk. /// /// Data access in default (L1) trait method implementations. fn provider(&self) -> impl BlockReaderIdExt; /// Returns a handle for forwarding received raw transactions. /// /// Access to transaction forwarder in default (L1) trait method implementations. fn raw_tx_forwarder(&self) -> Option>; /// Returns a handle for signing data. /// /// Singer access in default (L1) trait method implementations. fn signers(&self) -> &parking_lot::RwLock>>; /// Returns the transaction by hash. /// /// Checks the pool and state. /// /// Returns `Ok(None)` if no matching transaction was found. fn transaction_by_hash( &self, hash: B256, ) -> impl Future, Self::Error>> + Send { LoadTransaction::transaction_by_hash(self, hash) } /// Get all transactions in the block with the given hash. /// /// Returns `None` if block does not exist. fn transactions_by_block( &self, block: B256, ) -> impl Future>, Self::Error>> + Send { async move { self.cache().get_block_transactions(block).await.map_err(Self::Error::from_eth_err) } } /// Returns the EIP-2718 encoded transaction by hash. /// /// If this is a pooled EIP-4844 transaction, the blob sidecar is included. /// /// Checks the pool and state. /// /// Returns `Ok(None)` if no matching transaction was found. fn raw_transaction_by_hash( &self, hash: B256, ) -> impl Future, Self::Error>> + Send { async move { // Note: this is mostly used to fetch pooled transactions so we check the pool first if let Some(tx) = self.pool().get_pooled_transaction_element(hash).map(|tx| tx.envelope_encoded()) { return Ok(Some(tx)) } self.spawn_blocking_io(move |ref this| { Ok(LoadTransaction::provider(this) .transaction_by_hash(hash) .map_err(Self::Error::from_eth_err)? .map(|tx| tx.envelope_encoded())) }) .await } } /// Returns the _historical_ transaction and the block it was mined in fn historical_transaction_by_hash_at( &self, hash: B256, ) -> impl Future, Self::Error>> + Send { async move { match self.transaction_by_hash_at(hash).await? { None => Ok(None), Some((tx, at)) => Ok(at.as_block_hash().map(|hash| (tx, hash))), } } } /// Returns the transaction receipt for the given hash. /// /// Returns None if the transaction does not exist or is pending /// Note: The tx receipt is not available for pending transactions. fn transaction_receipt( &self, hash: B256, ) -> impl Future, Self::Error>> + Send where Self: LoadReceipt + 'static, { async move { match self.load_transaction_and_receipt(hash).await? { Some((tx, meta, receipt)) => { self.build_transaction_receipt(tx, meta, receipt).await.map(Some) } None => Ok(None), } } } /// Helper method that loads a transaction and its receipt. fn load_transaction_and_receipt( &self, hash: TxHash, ) -> impl Future< Output = Result, Self::Error>, > + Send where Self: 'static, { let this = self.clone(); self.spawn_blocking_io(move |_| { let (tx, meta) = match LoadTransaction::provider(&this) .transaction_by_hash_with_meta(hash) .map_err(Self::Error::from_eth_err)? { Some((tx, meta)) => (tx, meta), None => return Ok(None), }; let receipt = match EthTransactions::provider(&this) .receipt_by_hash(hash) .map_err(Self::Error::from_eth_err)? { Some(recpt) => recpt, None => return Ok(None), }; Ok(Some((tx, meta, receipt))) }) } /// Get [`Transaction`] by [`BlockId`] and index of transaction within that block. /// /// Returns `Ok(None)` if the block does not exist, or index is out of range. fn transaction_by_block_and_tx_index( &self, block_id: BlockId, index: usize, ) -> impl Future, Self::Error>> + Send where Self: LoadBlock, { async move { if let Some(block) = self.block_with_senders(block_id).await? { let block_hash = block.hash(); let block_number = block.number; let base_fee_per_gas = block.base_fee_per_gas; if let Some(tx) = block.into_transactions_ecrecovered().nth(index) { return Ok(Some(from_recovered_with_block_context( tx, block_hash, block_number, base_fee_per_gas, index, ))) } } Ok(None) } } /// Get transaction, as raw bytes, by [`BlockId`] and index of transaction within that block. /// /// Returns `Ok(None)` if the block does not exist, or index is out of range. fn raw_transaction_by_block_and_tx_index( &self, block_id: BlockId, index: usize, ) -> impl Future, Self::Error>> + Send where Self: LoadBlock, { async move { if let Some(block) = self.block_with_senders(block_id).await? { if let Some(tx) = block.transactions().nth(index) { return Ok(Some(tx.envelope_encoded())) } } Ok(None) } } /// Decodes and recovers the transaction and submits it to the pool. /// /// Returns the hash of the transaction. fn send_raw_transaction( &self, tx: Bytes, ) -> impl Future> + Send { async move { let recovered = recover_raw_transaction(tx.clone())?; let pool_transaction = ::Transaction::from_pooled(recovered); // On optimism, transactions are forwarded directly to the sequencer to be included in // blocks that it builds. if let Some(client) = self.raw_tx_forwarder().as_ref() { tracing::debug!( target: "rpc::eth", "forwarding raw transaction to"); let _ = client.forward_raw_transaction(&tx).await.inspect_err(|err| { tracing::debug!(target: "rpc::eth", %err, hash=% *pool_transaction.hash(), "failed to forward raw transaction"); }); } // submit the transaction to the pool with a `Local` origin let hash = self .pool() .add_transaction(TransactionOrigin::Local, pool_transaction) .await .map_err(Self::Error::from_eth_err)?; Ok(hash) } } /// Signs transaction with a matching signer, if any and submits the transaction to the pool. /// Returns the hash of the signed transaction. fn send_transaction( &self, mut request: TransactionRequest, ) -> impl Future> + Send where Self: EthApiSpec + LoadBlock + LoadPendingBlock + LoadFee + Call, { async move { let from = match request.from { Some(from) => from, None => return Err(SignError::NoAccount.into_eth_err()), }; if self.find_signer(&from).is_err() { return Err(SignError::NoAccount.into_eth_err()); } // set nonce if not already set before if request.nonce.is_none() { let nonce = self.transaction_count(from, Some(BlockId::pending())).await?; // note: `.to()` can't panic because the nonce is constructed from a `u64` request.nonce = Some(nonce.to::()); } let chain_id = self.chain_id(); let estimated_gas = self.estimate_gas_at(request.clone(), BlockId::pending(), None).await?; let gas_limit = estimated_gas; let TransactionRequest { to, gas_price, max_fee_per_gas, max_priority_fee_per_gas, gas, value, input: data, nonce, mut access_list, max_fee_per_blob_gas, blob_versioned_hashes, sidecar, .. } = request; // todo: remove this inlining after https://github.com/alloy-rs/alloy/pull/183#issuecomment-1928161285 let transaction = match ( gas_price, max_fee_per_gas, access_list.take(), max_fee_per_blob_gas, blob_versioned_hashes, sidecar, ) { // legacy transaction // gas price required (Some(_), None, None, None, None, None) => { Some(TypedTransactionRequest::Legacy(LegacyTransactionRequest { nonce: nonce.unwrap_or_default(), gas_price: U256::from(gas_price.unwrap_or_default()), gas_limit: U256::from(gas.unwrap_or_default()), value: value.unwrap_or_default(), input: data.into_input().unwrap_or_default(), kind: to.unwrap_or(TxKind::Create), chain_id: None, })) } // EIP2930 // if only accesslist is set, and no eip1599 fees (_, None, Some(access_list), None, None, None) => { Some(TypedTransactionRequest::EIP2930(EIP2930TransactionRequest { nonce: nonce.unwrap_or_default(), gas_price: U256::from(gas_price.unwrap_or_default()), gas_limit: U256::from(gas.unwrap_or_default()), value: value.unwrap_or_default(), input: data.into_input().unwrap_or_default(), kind: to.unwrap_or(TxKind::Create), chain_id: 0, access_list, })) } // EIP1559 // if 4844 fields missing // gas_price, max_fee_per_gas, access_list, max_fee_per_blob_gas, // blob_versioned_hashes, sidecar, (None, _, _, None, None, None) => { // Empty fields fall back to the canonical transaction schema. Some(TypedTransactionRequest::EIP1559(EIP1559TransactionRequest { nonce: nonce.unwrap_or_default(), max_fee_per_gas: U256::from(max_fee_per_gas.unwrap_or_default()), max_priority_fee_per_gas: U256::from( max_priority_fee_per_gas.unwrap_or_default(), ), gas_limit: U256::from(gas.unwrap_or_default()), value: value.unwrap_or_default(), input: data.into_input().unwrap_or_default(), kind: to.unwrap_or(TxKind::Create), chain_id: 0, access_list: access_list.unwrap_or_default(), })) } // EIP4884 // all blob fields required ( None, _, _, Some(max_fee_per_blob_gas), Some(blob_versioned_hashes), Some(sidecar), ) => { // As per the EIP, we follow the same semantics as EIP-1559. Some(TypedTransactionRequest::EIP4844(EIP4844TransactionRequest { chain_id: 0, nonce: nonce.unwrap_or_default(), max_priority_fee_per_gas: U256::from( max_priority_fee_per_gas.unwrap_or_default(), ), max_fee_per_gas: U256::from(max_fee_per_gas.unwrap_or_default()), gas_limit: U256::from(gas.unwrap_or_default()), value: value.unwrap_or_default(), input: data.into_input().unwrap_or_default(), to: match to { Some(TxKind::Call(to)) => to, _ => Address::default(), }, access_list: access_list.unwrap_or_default(), // eip-4844 specific. max_fee_per_blob_gas: U256::from(max_fee_per_blob_gas), blob_versioned_hashes, sidecar, })) } _ => None, }; let transaction = match transaction { Some(TypedTransactionRequest::Legacy(mut req)) => { req.chain_id = Some(chain_id.to()); req.gas_limit = gas_limit.saturating_to(); req.gas_price = self.legacy_gas_price(gas_price.map(U256::from)).await?; TypedTransactionRequest::Legacy(req) } Some(TypedTransactionRequest::EIP2930(mut req)) => { req.chain_id = chain_id.to(); req.gas_limit = gas_limit.saturating_to(); req.gas_price = self.legacy_gas_price(gas_price.map(U256::from)).await?; TypedTransactionRequest::EIP2930(req) } Some(TypedTransactionRequest::EIP1559(mut req)) => { let (max_fee_per_gas, max_priority_fee_per_gas) = self .eip1559_fees( max_fee_per_gas.map(U256::from), max_priority_fee_per_gas.map(U256::from), ) .await?; req.chain_id = chain_id.to(); req.gas_limit = gas_limit.saturating_to(); req.max_fee_per_gas = max_fee_per_gas.saturating_to(); req.max_priority_fee_per_gas = max_priority_fee_per_gas.saturating_to(); TypedTransactionRequest::EIP1559(req) } Some(TypedTransactionRequest::EIP4844(mut req)) => { let (max_fee_per_gas, max_priority_fee_per_gas) = self .eip1559_fees( max_fee_per_gas.map(U256::from), max_priority_fee_per_gas.map(U256::from), ) .await?; req.max_fee_per_gas = max_fee_per_gas; req.max_priority_fee_per_gas = max_priority_fee_per_gas; req.max_fee_per_blob_gas = self.eip4844_blob_fee(max_fee_per_blob_gas.map(U256::from)).await?; req.chain_id = chain_id.to(); req.gas_limit = gas_limit; TypedTransactionRequest::EIP4844(req) } None => return Err(EthApiError::ConflictingFeeFieldsInRequest.into()), }; let signed_tx = self.sign_request(&from, transaction)?; let recovered = signed_tx.into_ecrecovered().ok_or(EthApiError::InvalidTransactionSignature)?; let pool_transaction = match recovered.try_into() { Ok(converted) => converted, Err(_) => return Err(EthApiError::TransactionConversionError.into()), }; // submit the transaction to the pool with a `Local` origin let hash = LoadTransaction::pool(self) .add_transaction(TransactionOrigin::Local, pool_transaction) .await .map_err(Self::Error::from_eth_err)?; Ok(hash) } } /// Signs a transaction, with configured signers. fn sign_request( &self, from: &Address, request: TypedTransactionRequest, ) -> Result { for signer in self.signers().read().iter() { if signer.is_signer_for(from) { return match signer.sign_transaction(request, from) { Ok(tx) => Ok(tx), Err(e) => Err(e.into_eth_err()), } } } Err(EthApiError::InvalidTransactionSignature.into()) } /// Signs given message. Returns the signature. fn sign( &self, account: Address, message: Bytes, ) -> impl Future> + Send { async move { Ok(self .find_signer(&account)? .sign(account, &message) .await .map_err(Self::Error::from_eth_err)? .to_hex_bytes()) } } /// Encodes and signs the typed data according EIP-712. Payload must implement Eip712 trait. fn sign_typed_data(&self, data: &TypedData, account: Address) -> Result { Ok(self .find_signer(&account)? .sign_typed_data(account, data) .map_err(Self::Error::from_eth_err)? .to_hex_bytes()) } /// Returns the signer for the given account, if found in configured signers. fn find_signer( &self, account: &Address, ) -> Result, Self::Error> { self.signers() .read() .iter() .find(|signer| signer.is_signer_for(account)) .map(|signer| dyn_clone::clone_box(&**signer)) .ok_or_else(|| SignError::NoAccount.into_eth_err()) } } /// Loads a transaction from database. /// /// Behaviour shared by several `eth_` RPC methods, not exclusive to `eth_` transactions RPC /// methods. pub trait LoadTransaction: SpawnBlocking { /// Transaction pool with pending transactions. [`TransactionPool::Transaction`] is the /// supported transaction type. type Pool: TransactionPool; /// Returns a handle for reading data from disk. /// /// Data access in default (L1) trait method implementations. fn provider(&self) -> impl TransactionsProvider; /// Returns a handle for reading data from memory. /// /// Data access in default (L1) trait method implementations. fn cache(&self) -> &EthStateCache; /// Returns a handle for reading data from pool. /// /// Data access in default (L1) trait method implementations. fn pool(&self) -> &Self::Pool; /// Returns the transaction by hash. /// /// Checks the pool and state. /// /// Returns `Ok(None)` if no matching transaction was found. fn transaction_by_hash( &self, hash: B256, ) -> impl Future, Self::Error>> + Send { async move { // Try to find the transaction on disk let mut resp = self .spawn_blocking_io(move |this| { match this .provider() .transaction_by_hash_with_meta(hash) .map_err(Self::Error::from_eth_err)? { None => Ok(None), Some((tx, meta)) => { // Note: we assume this transaction is valid, because it's mined (or // part of pending block) and already. We don't need to // check for pre EIP-2 because this transaction could be pre-EIP-2. let transaction = tx .into_ecrecovered_unchecked() .ok_or(EthApiError::InvalidTransactionSignature)?; let tx = TransactionSource::Block { transaction, index: meta.index, block_hash: meta.block_hash, block_number: meta.block_number, base_fee: meta.base_fee, }; Ok(Some(tx)) } } }) .await?; if resp.is_none() { // tx not found on disk, check pool if let Some(tx) = self.pool().get(&hash).map(|tx| tx.transaction.clone().into()) { resp = Some(TransactionSource::Pool(tx)); } } Ok(resp) } } /// Returns the transaction by including its corresponding [`BlockId`]. /// /// Note: this supports pending transactions fn transaction_by_hash_at( &self, transaction_hash: B256, ) -> impl Future, Self::Error>> + Send { async move { Ok(self.transaction_by_hash(transaction_hash).await?.map(|tx| match tx { tx @ TransactionSource::Pool(_) => (tx, BlockId::pending()), tx @ TransactionSource::Block { block_hash, .. } => { (tx, BlockId::Hash(block_hash.into())) } })) } } /// Fetches the transaction and the transaction's block fn transaction_and_block( &self, hash: B256, ) -> impl Future, Self::Error>> + Send { async move { let (transaction, at) = match self.transaction_by_hash_at(hash).await? { None => return Ok(None), Some(res) => res, }; // Note: this is always either hash or pending let block_hash = match at { BlockId::Hash(hash) => hash.block_hash, _ => return Ok(None), }; let block = self .cache() .get_block_with_senders(block_hash) .await .map_err(Self::Error::from_eth_err)?; Ok(block.map(|block| (transaction, block.seal(block_hash)))) } } } /// A trait that allows for forwarding raw transactions. /// /// For example to a sequencer. #[async_trait::async_trait] pub trait RawTransactionForwarder: fmt::Debug + Send + Sync + 'static { /// Forwards raw transaction bytes for `eth_sendRawTransaction` async fn forward_raw_transaction(&self, raw: &[u8]) -> EthResult<()>; } /// Configure server's forwarder for `eth_sendRawTransaction`, at runtime. pub trait UpdateRawTxForwarder { /// Sets a forwarder for `eth_sendRawTransaction` /// /// Note: this might be removed in the future in favor of a more generic approach. fn set_eth_raw_transaction_forwarder(&self, forwarder: Arc); } impl UpdateRawTxForwarder for T where T: Deref>, K: UpdateRawTxForwarder, { fn set_eth_raw_transaction_forwarder(&self, forwarder: Arc) { self.deref().deref().set_eth_raw_transaction_forwarder(forwarder); } }