reth_rpc_eth_api/helpers/
transaction.rs

1//! Database access for `eth_` transaction RPC methods. Loads transaction and receipt data w.r.t.
2//! network.
3
4use super::{EthApiSpec, EthSigner, LoadBlock, LoadReceipt, LoadState, SpawnBlocking};
5use crate::{
6    helpers::estimate::EstimateCall, FromEthApiError, FullEthApiTypes, IntoEthApiError,
7    RpcNodeCore, RpcNodeCoreExt, RpcReceipt, RpcTransaction,
8};
9use alloy_consensus::{
10    transaction::{SignerRecoverable, TransactionMeta},
11    BlockHeader, Transaction,
12};
13use alloy_dyn_abi::TypedData;
14use alloy_eips::{eip2718::Encodable2718, BlockId};
15use alloy_network::TransactionBuilder;
16use alloy_primitives::{Address, Bytes, TxHash, B256};
17use alloy_rpc_types_eth::{transaction::TransactionRequest, BlockNumberOrTag, TransactionInfo};
18use futures::Future;
19use reth_node_api::BlockBody;
20use reth_primitives_traits::{RecoveredBlock, SignedTransaction};
21use reth_rpc_eth_types::{utils::binary_search, EthApiError, SignError, TransactionSource};
22use reth_rpc_types_compat::transaction::TransactionCompat;
23use reth_storage_api::{
24    BlockNumReader, BlockReaderIdExt, ProviderBlock, ProviderReceipt, ProviderTx, ReceiptProvider,
25    TransactionsProvider,
26};
27use reth_transaction_pool::{PoolTransaction, TransactionOrigin, TransactionPool};
28use std::sync::Arc;
29
30/// Transaction related functions for the [`EthApiServer`](crate::EthApiServer) trait in
31/// the `eth_` namespace.
32///
33/// This includes utilities for transaction tracing, transacting and inspection.
34///
35/// Async functions that are spawned onto the
36/// [`BlockingTaskPool`](reth_tasks::pool::BlockingTaskPool) begin with `spawn_`
37///
38/// ## Calls
39///
40/// There are subtle differences between when transacting [`TransactionRequest`]:
41///
42/// The endpoints `eth_call` and `eth_estimateGas` and `eth_createAccessList` should always
43/// __disable__ the base fee check in the [`CfgEnv`](revm::context::CfgEnv).
44///
45/// The behaviour for tracing endpoints is not consistent across clients.
46/// Geth also disables the basefee check for tracing: <https://github.com/ethereum/go-ethereum/blob/bc0b87ca196f92e5af49bd33cc190ef0ec32b197/eth/tracers/api.go#L955-L955>
47/// Erigon does not: <https://github.com/ledgerwatch/erigon/blob/aefb97b07d1c4fd32a66097a24eddd8f6ccacae0/turbo/transactions/tracing.go#L209-L209>
48///
49/// See also <https://github.com/paradigmxyz/reth/issues/6240>
50///
51/// This implementation follows the behaviour of Geth and disables the basefee check for tracing.
52pub trait EthTransactions: LoadTransaction<Provider: BlockReaderIdExt> {
53    /// Returns a handle for signing data.
54    ///
55    /// Signer access in default (L1) trait method implementations.
56    #[expect(clippy::type_complexity)]
57    fn signers(&self) -> &parking_lot::RwLock<Vec<Box<dyn EthSigner<ProviderTx<Self::Provider>>>>>;
58
59    /// Decodes and recovers the transaction and submits it to the pool.
60    ///
61    /// Returns the hash of the transaction.
62    fn send_raw_transaction(
63        &self,
64        tx: Bytes,
65    ) -> impl Future<Output = Result<B256, Self::Error>> + Send;
66
67    /// Returns the transaction by hash.
68    ///
69    /// Checks the pool and state.
70    ///
71    /// Returns `Ok(None)` if no matching transaction was found.
72    #[expect(clippy::complexity)]
73    fn transaction_by_hash(
74        &self,
75        hash: B256,
76    ) -> impl Future<
77        Output = Result<Option<TransactionSource<ProviderTx<Self::Provider>>>, Self::Error>,
78    > + Send {
79        LoadTransaction::transaction_by_hash(self, hash)
80    }
81
82    /// Get all transactions in the block with the given hash.
83    ///
84    /// Returns `None` if block does not exist.
85    #[expect(clippy::type_complexity)]
86    fn transactions_by_block(
87        &self,
88        block: B256,
89    ) -> impl Future<Output = Result<Option<Vec<ProviderTx<Self::Provider>>>, Self::Error>> + Send
90    {
91        async move {
92            self.cache()
93                .get_recovered_block(block)
94                .await
95                .map(|b| b.map(|b| b.body().transactions().to_vec()))
96                .map_err(Self::Error::from_eth_err)
97        }
98    }
99
100    /// Returns the EIP-2718 encoded transaction by hash.
101    ///
102    /// If this is a pooled EIP-4844 transaction, the blob sidecar is included.
103    ///
104    /// Checks the pool and state.
105    ///
106    /// Returns `Ok(None)` if no matching transaction was found.
107    fn raw_transaction_by_hash(
108        &self,
109        hash: B256,
110    ) -> impl Future<Output = Result<Option<Bytes>, Self::Error>> + Send {
111        async move {
112            // Note: this is mostly used to fetch pooled transactions so we check the pool first
113            if let Some(tx) =
114                self.pool().get_pooled_transaction_element(hash).map(|tx| tx.encoded_2718().into())
115            {
116                return Ok(Some(tx))
117            }
118
119            self.spawn_blocking_io(move |ref this| {
120                Ok(this
121                    .provider()
122                    .transaction_by_hash(hash)
123                    .map_err(Self::Error::from_eth_err)?
124                    .map(|tx| tx.encoded_2718().into()))
125            })
126            .await
127        }
128    }
129
130    /// Returns the _historical_ transaction and the block it was mined in
131    #[expect(clippy::type_complexity)]
132    fn historical_transaction_by_hash_at(
133        &self,
134        hash: B256,
135    ) -> impl Future<
136        Output = Result<Option<(TransactionSource<ProviderTx<Self::Provider>>, B256)>, Self::Error>,
137    > + Send {
138        async move {
139            match self.transaction_by_hash_at(hash).await? {
140                None => Ok(None),
141                Some((tx, at)) => Ok(at.as_block_hash().map(|hash| (tx, hash))),
142            }
143        }
144    }
145
146    /// Returns the transaction receipt for the given hash.
147    ///
148    /// Returns None if the transaction does not exist or is pending
149    /// Note: The tx receipt is not available for pending transactions.
150    fn transaction_receipt(
151        &self,
152        hash: B256,
153    ) -> impl Future<Output = Result<Option<RpcReceipt<Self::NetworkTypes>>, Self::Error>> + Send
154    where
155        Self: LoadReceipt + 'static,
156    {
157        async move {
158            match self.load_transaction_and_receipt(hash).await? {
159                Some((tx, meta, receipt)) => {
160                    self.build_transaction_receipt(tx, meta, receipt).await.map(Some)
161                }
162                None => Ok(None),
163            }
164        }
165    }
166
167    /// Helper method that loads a transaction and its receipt.
168    #[expect(clippy::complexity)]
169    fn load_transaction_and_receipt(
170        &self,
171        hash: TxHash,
172    ) -> impl Future<
173        Output = Result<
174            Option<(ProviderTx<Self::Provider>, TransactionMeta, ProviderReceipt<Self::Provider>)>,
175            Self::Error,
176        >,
177    > + Send
178    where
179        Self: 'static,
180    {
181        let provider = self.provider().clone();
182        self.spawn_blocking_io(move |_| {
183            let (tx, meta) = match provider
184                .transaction_by_hash_with_meta(hash)
185                .map_err(Self::Error::from_eth_err)?
186            {
187                Some((tx, meta)) => (tx, meta),
188                None => return Ok(None),
189            };
190
191            let receipt = match provider.receipt_by_hash(hash).map_err(Self::Error::from_eth_err)? {
192                Some(recpt) => recpt,
193                None => return Ok(None),
194            };
195
196            Ok(Some((tx, meta, receipt)))
197        })
198    }
199
200    /// Get transaction by [`BlockId`] and index of transaction within that block.
201    ///
202    /// Returns `Ok(None)` if the block does not exist, or index is out of range.
203    fn transaction_by_block_and_tx_index(
204        &self,
205        block_id: BlockId,
206        index: usize,
207    ) -> impl Future<Output = Result<Option<RpcTransaction<Self::NetworkTypes>>, Self::Error>> + Send
208    where
209        Self: LoadBlock,
210    {
211        async move {
212            if let Some(block) = self.recovered_block(block_id).await? {
213                let block_hash = block.hash();
214                let block_number = block.number();
215                let base_fee_per_gas = block.base_fee_per_gas();
216                if let Some((signer, tx)) = block.transactions_with_sender().nth(index) {
217                    let tx_info = TransactionInfo {
218                        hash: Some(*tx.tx_hash()),
219                        block_hash: Some(block_hash),
220                        block_number: Some(block_number),
221                        base_fee: base_fee_per_gas,
222                        index: Some(index as u64),
223                    };
224
225                    return Ok(Some(
226                        self.tx_resp_builder().fill(tx.clone().with_signer(*signer), tx_info)?,
227                    ))
228                }
229            }
230
231            Ok(None)
232        }
233    }
234
235    /// Find a transaction by sender's address and nonce.
236    fn get_transaction_by_sender_and_nonce(
237        &self,
238        sender: Address,
239        nonce: u64,
240        include_pending: bool,
241    ) -> impl Future<Output = Result<Option<RpcTransaction<Self::NetworkTypes>>, Self::Error>> + Send
242    where
243        Self: LoadBlock + LoadState,
244    {
245        async move {
246            // Check the pool first
247            if include_pending {
248                if let Some(tx) =
249                    RpcNodeCore::pool(self).get_transaction_by_sender_and_nonce(sender, nonce)
250                {
251                    let transaction = tx.transaction.clone_into_consensus();
252                    return Ok(Some(self.tx_resp_builder().fill_pending(transaction)?));
253                }
254            }
255
256            // Check if the sender is a contract
257            if !self.get_code(sender, None).await?.is_empty() {
258                return Ok(None);
259            }
260
261            let highest = self.transaction_count(sender, None).await?.saturating_to::<u64>();
262
263            // If the nonce is higher or equal to the highest nonce, the transaction is pending or
264            // not exists.
265            if nonce >= highest {
266                return Ok(None);
267            }
268
269            let Ok(high) = self.provider().best_block_number() else {
270                return Err(EthApiError::HeaderNotFound(BlockNumberOrTag::Latest.into()).into());
271            };
272
273            // Perform a binary search over the block range to find the block in which the sender's
274            // nonce reached the requested nonce.
275            let num = binary_search::<_, _, Self::Error>(1, high, |mid| async move {
276                let mid_nonce =
277                    self.transaction_count(sender, Some(mid.into())).await?.saturating_to::<u64>();
278
279                Ok(mid_nonce > nonce)
280            })
281            .await?;
282
283            let block_id = num.into();
284            self.recovered_block(block_id)
285                .await?
286                .and_then(|block| {
287                    let block_hash = block.hash();
288                    let block_number = block.number();
289                    let base_fee_per_gas = block.base_fee_per_gas();
290
291                    block
292                        .transactions_with_sender()
293                        .enumerate()
294                        .find(|(_, (signer, tx))| **signer == sender && (*tx).nonce() == nonce)
295                        .map(|(index, (signer, tx))| {
296                            let tx_info = TransactionInfo {
297                                hash: Some(*tx.tx_hash()),
298                                block_hash: Some(block_hash),
299                                block_number: Some(block_number),
300                                base_fee: base_fee_per_gas,
301                                index: Some(index as u64),
302                            };
303                            self.tx_resp_builder().fill(tx.clone().with_signer(*signer), tx_info)
304                        })
305                })
306                .ok_or(EthApiError::HeaderNotFound(block_id))?
307                .map(Some)
308        }
309    }
310
311    /// Get transaction, as raw bytes, by [`BlockId`] and index of transaction within that block.
312    ///
313    /// Returns `Ok(None)` if the block does not exist, or index is out of range.
314    fn raw_transaction_by_block_and_tx_index(
315        &self,
316        block_id: BlockId,
317        index: usize,
318    ) -> impl Future<Output = Result<Option<Bytes>, Self::Error>> + Send
319    where
320        Self: LoadBlock,
321    {
322        async move {
323            if let Some(block) = self.recovered_block(block_id).await? {
324                if let Some(tx) = block.body().transactions().get(index) {
325                    return Ok(Some(tx.encoded_2718().into()))
326                }
327            }
328
329            Ok(None)
330        }
331    }
332
333    /// Signs transaction with a matching signer, if any and submits the transaction to the pool.
334    /// Returns the hash of the signed transaction.
335    fn send_transaction(
336        &self,
337        mut request: TransactionRequest,
338    ) -> impl Future<Output = Result<B256, Self::Error>> + Send
339    where
340        Self: EthApiSpec + LoadBlock + EstimateCall,
341    {
342        async move {
343            let from = match request.from {
344                Some(from) => from,
345                None => return Err(SignError::NoAccount.into_eth_err()),
346            };
347
348            if self.find_signer(&from).is_err() {
349                return Err(SignError::NoAccount.into_eth_err())
350            }
351
352            // set nonce if not already set before
353            if request.nonce.is_none() {
354                let nonce = self.next_available_nonce(from).await?;
355                request.nonce = Some(nonce);
356            }
357
358            let chain_id = self.chain_id();
359            request.chain_id = Some(chain_id.to());
360
361            let estimated_gas =
362                self.estimate_gas_at(request.clone(), BlockId::pending(), None).await?;
363            let gas_limit = estimated_gas;
364            request.set_gas_limit(gas_limit.to());
365
366            let transaction = self.sign_request(&from, request).await?.with_signer(from);
367
368            let pool_transaction =
369                <<Self as RpcNodeCore>::Pool as TransactionPool>::Transaction::try_from_consensus(
370                    transaction,
371                )
372                .map_err(|_| EthApiError::TransactionConversionError)?;
373
374            // submit the transaction to the pool with a `Local` origin
375            let hash = self
376                .pool()
377                .add_transaction(TransactionOrigin::Local, pool_transaction)
378                .await
379                .map_err(Self::Error::from_eth_err)?;
380
381            Ok(hash)
382        }
383    }
384
385    /// Signs a transaction, with configured signers.
386    fn sign_request(
387        &self,
388        from: &Address,
389        txn: TransactionRequest,
390    ) -> impl Future<Output = Result<ProviderTx<Self::Provider>, Self::Error>> + Send {
391        async move {
392            self.find_signer(from)?
393                .sign_transaction(txn, from)
394                .await
395                .map_err(Self::Error::from_eth_err)
396        }
397    }
398
399    /// Signs given message. Returns the signature.
400    fn sign(
401        &self,
402        account: Address,
403        message: Bytes,
404    ) -> impl Future<Output = Result<Bytes, Self::Error>> + Send {
405        async move {
406            Ok(self
407                .find_signer(&account)?
408                .sign(account, &message)
409                .await
410                .map_err(Self::Error::from_eth_err)?
411                .as_bytes()
412                .into())
413        }
414    }
415
416    /// Signs a transaction request using the given account in request
417    /// Returns the EIP-2718 encoded signed transaction.
418    fn sign_transaction(
419        &self,
420        request: TransactionRequest,
421    ) -> impl Future<Output = Result<Bytes, Self::Error>> + Send {
422        async move {
423            let from = match request.from {
424                Some(from) => from,
425                None => return Err(SignError::NoAccount.into_eth_err()),
426            };
427
428            Ok(self.sign_request(&from, request).await?.encoded_2718().into())
429        }
430    }
431
432    /// Encodes and signs the typed data according EIP-712. Payload must implement Eip712 trait.
433    fn sign_typed_data(&self, data: &TypedData, account: Address) -> Result<Bytes, Self::Error> {
434        Ok(self
435            .find_signer(&account)?
436            .sign_typed_data(account, data)
437            .map_err(Self::Error::from_eth_err)?
438            .as_bytes()
439            .into())
440    }
441
442    /// Returns the signer for the given account, if found in configured signers.
443    #[expect(clippy::type_complexity)]
444    fn find_signer(
445        &self,
446        account: &Address,
447    ) -> Result<Box<(dyn EthSigner<ProviderTx<Self::Provider>> + 'static)>, Self::Error> {
448        self.signers()
449            .read()
450            .iter()
451            .find(|signer| signer.is_signer_for(account))
452            .map(|signer| dyn_clone::clone_box(&**signer))
453            .ok_or_else(|| SignError::NoAccount.into_eth_err())
454    }
455}
456
457/// Loads a transaction from database.
458///
459/// Behaviour shared by several `eth_` RPC methods, not exclusive to `eth_` transactions RPC
460/// methods.
461pub trait LoadTransaction: SpawnBlocking + FullEthApiTypes + RpcNodeCoreExt {
462    /// Returns the transaction by hash.
463    ///
464    /// Checks the pool and state.
465    ///
466    /// Returns `Ok(None)` if no matching transaction was found.
467    #[expect(clippy::complexity)]
468    fn transaction_by_hash(
469        &self,
470        hash: B256,
471    ) -> impl Future<
472        Output = Result<Option<TransactionSource<ProviderTx<Self::Provider>>>, Self::Error>,
473    > + Send {
474        async move {
475            // Try to find the transaction on disk
476            let mut resp = self
477                .spawn_blocking_io(move |this| {
478                    match this
479                        .provider()
480                        .transaction_by_hash_with_meta(hash)
481                        .map_err(Self::Error::from_eth_err)?
482                    {
483                        None => Ok(None),
484                        Some((tx, meta)) => {
485                            // Note: we assume this transaction is valid, because it's mined (or
486                            // part of pending block) and already. We don't need to
487                            // check for pre EIP-2 because this transaction could be pre-EIP-2.
488                            let transaction = tx
489                                .try_into_recovered_unchecked()
490                                .map_err(|_| EthApiError::InvalidTransactionSignature)?;
491
492                            let tx = TransactionSource::Block {
493                                transaction,
494                                index: meta.index,
495                                block_hash: meta.block_hash,
496                                block_number: meta.block_number,
497                                base_fee: meta.base_fee,
498                            };
499                            Ok(Some(tx))
500                        }
501                    }
502                })
503                .await?;
504
505            if resp.is_none() {
506                // tx not found on disk, check pool
507                if let Some(tx) =
508                    self.pool().get(&hash).map(|tx| tx.transaction.clone().into_consensus())
509                {
510                    resp = Some(TransactionSource::Pool(tx.into()));
511                }
512            }
513
514            Ok(resp)
515        }
516    }
517
518    /// Returns the transaction by including its corresponding [`BlockId`].
519    ///
520    /// Note: this supports pending transactions
521    #[expect(clippy::type_complexity)]
522    fn transaction_by_hash_at(
523        &self,
524        transaction_hash: B256,
525    ) -> impl Future<
526        Output = Result<
527            Option<(TransactionSource<ProviderTx<Self::Provider>>, BlockId)>,
528            Self::Error,
529        >,
530    > + Send {
531        async move {
532            Ok(self.transaction_by_hash(transaction_hash).await?.map(|tx| match tx {
533                tx @ TransactionSource::Pool(_) => (tx, BlockId::pending()),
534                tx @ TransactionSource::Block { block_hash, .. } => {
535                    (tx, BlockId::Hash(block_hash.into()))
536                }
537            }))
538        }
539    }
540
541    /// Fetches the transaction and the transaction's block
542    #[expect(clippy::type_complexity)]
543    fn transaction_and_block(
544        &self,
545        hash: B256,
546    ) -> impl Future<
547        Output = Result<
548            Option<(
549                TransactionSource<ProviderTx<Self::Provider>>,
550                Arc<RecoveredBlock<ProviderBlock<Self::Provider>>>,
551            )>,
552            Self::Error,
553        >,
554    > + Send {
555        async move {
556            let (transaction, at) = match self.transaction_by_hash_at(hash).await? {
557                None => return Ok(None),
558                Some(res) => res,
559            };
560
561            // Note: this is always either hash or pending
562            let block_hash = match at {
563                BlockId::Hash(hash) => hash.block_hash,
564                _ => return Ok(None),
565            };
566            let block = self
567                .cache()
568                .get_recovered_block(block_hash)
569                .await
570                .map_err(Self::Error::from_eth_err)?;
571            Ok(block.map(|block| (transaction, block)))
572        }
573    }
574}