mirror of
https://github.com/hl-archive-node/nanoreth.git
synced 2025-12-06 10:59:55 +00:00
584 lines
21 KiB
Rust
584 lines
21 KiB
Rust
//! Database access for `eth_` transaction RPC methods. Loads transaction and receipt data w.r.t.
|
|
//! network.
|
|
|
|
use alloy_consensus::{BlockHeader, Transaction};
|
|
use alloy_dyn_abi::TypedData;
|
|
use alloy_eips::{eip2718::Encodable2718, BlockId};
|
|
use alloy_network::TransactionBuilder;
|
|
use alloy_primitives::{Address, Bytes, TxHash, B256};
|
|
use alloy_rpc_types_eth::{transaction::TransactionRequest, BlockNumberOrTag, TransactionInfo};
|
|
use futures::Future;
|
|
use reth_primitives::{SealedBlockWithSenders, TransactionMeta, TransactionSigned};
|
|
use reth_provider::{
|
|
BlockNumReader, BlockReaderIdExt, ProviderReceipt, ProviderTx, ReceiptProvider,
|
|
TransactionsProvider,
|
|
};
|
|
use reth_rpc_eth_types::{
|
|
utils::{binary_search, recover_raw_transaction},
|
|
EthApiError, SignError, TransactionSource,
|
|
};
|
|
use reth_rpc_types_compat::transaction::{from_recovered, from_recovered_with_block_context};
|
|
use reth_transaction_pool::{PoolTransaction, TransactionOrigin, TransactionPool};
|
|
use std::sync::Arc;
|
|
|
|
use super::{
|
|
EthApiSpec, EthSigner, LoadBlock, LoadPendingBlock, LoadReceipt, LoadState, SpawnBlocking,
|
|
};
|
|
use crate::{
|
|
helpers::estimate::EstimateCall, FromEthApiError, FullEthApiTypes, IntoEthApiError,
|
|
RpcNodeCore, RpcNodeCoreExt, RpcReceipt, RpcTransaction,
|
|
};
|
|
|
|
/// 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: <https://github.com/ethereum/go-ethereum/blob/bc0b87ca196f92e5af49bd33cc190ef0ec32b197/eth/tracers/api.go#L955-L955>
|
|
/// Erigon does not: <https://github.com/ledgerwatch/erigon/blob/aefb97b07d1c4fd32a66097a24eddd8f6ccacae0/turbo/transactions/tracing.go#L209-L209>
|
|
///
|
|
/// See also <https://github.com/paradigmxyz/reth/issues/6240>
|
|
///
|
|
/// This implementation follows the behaviour of Geth and disables the basefee check for tracing.
|
|
pub trait EthTransactions: LoadTransaction<Provider: BlockReaderIdExt> {
|
|
/// Returns a handle for signing data.
|
|
///
|
|
/// Singer access in default (L1) trait method implementations.
|
|
fn signers(&self) -> &parking_lot::RwLock<Vec<Box<dyn EthSigner>>>;
|
|
|
|
/// Returns the transaction by hash.
|
|
///
|
|
/// Checks the pool and state.
|
|
///
|
|
/// Returns `Ok(None)` if no matching transaction was found.
|
|
#[expect(clippy::complexity)]
|
|
fn transaction_by_hash(
|
|
&self,
|
|
hash: B256,
|
|
) -> impl Future<
|
|
Output = Result<Option<TransactionSource<ProviderTx<Self::Provider>>>, 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<Output = Result<Option<Vec<TransactionSigned>>, Self::Error>> + Send {
|
|
async move {
|
|
self.cache()
|
|
.get_sealed_block_with_senders(block)
|
|
.await
|
|
.map(|b| b.map(|b| b.body.transactions.clone()))
|
|
.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<Output = Result<Option<Bytes>, 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.encoded_2718().into())
|
|
{
|
|
return Ok(Some(tx))
|
|
}
|
|
|
|
self.spawn_blocking_io(move |ref this| {
|
|
Ok(this
|
|
.provider()
|
|
.transaction_by_hash(hash)
|
|
.map_err(Self::Error::from_eth_err)?
|
|
.map(|tx| tx.encoded_2718().into()))
|
|
})
|
|
.await
|
|
}
|
|
}
|
|
|
|
/// Returns the _historical_ transaction and the block it was mined in
|
|
fn historical_transaction_by_hash_at(
|
|
&self,
|
|
hash: B256,
|
|
) -> impl Future<Output = Result<Option<(TransactionSource, B256)>, 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<Output = Result<Option<RpcReceipt<Self::NetworkTypes>>, 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.
|
|
#[expect(clippy::complexity)]
|
|
fn load_transaction_and_receipt(
|
|
&self,
|
|
hash: TxHash,
|
|
) -> impl Future<
|
|
Output = Result<
|
|
Option<(ProviderTx<Self::Provider>, TransactionMeta, ProviderReceipt<Self::Provider>)>,
|
|
Self::Error,
|
|
>,
|
|
> + Send
|
|
where
|
|
Self: 'static,
|
|
{
|
|
let provider = self.provider().clone();
|
|
self.spawn_blocking_io(move |_| {
|
|
let (tx, meta) = match provider
|
|
.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 provider.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<Output = Result<Option<RpcTransaction<Self::NetworkTypes>>, 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((signer, tx)) = block.transactions_with_sender().nth(index) {
|
|
let tx_info = TransactionInfo {
|
|
hash: Some(tx.hash()),
|
|
block_hash: Some(block_hash),
|
|
block_number: Some(block_number),
|
|
base_fee: base_fee_per_gas.map(u128::from),
|
|
index: Some(index as u64),
|
|
};
|
|
|
|
return Ok(Some(from_recovered_with_block_context(
|
|
tx.clone().with_signer(*signer),
|
|
tx_info,
|
|
self.tx_resp_builder(),
|
|
)?))
|
|
}
|
|
}
|
|
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
/// Find a transaction by sender's address and nonce.
|
|
fn get_transaction_by_sender_and_nonce(
|
|
&self,
|
|
sender: Address,
|
|
nonce: u64,
|
|
include_pending: bool,
|
|
) -> impl Future<Output = Result<Option<RpcTransaction<Self::NetworkTypes>>, Self::Error>> + Send
|
|
where
|
|
Self: LoadBlock + LoadState,
|
|
{
|
|
async move {
|
|
// Check the pool first
|
|
if include_pending {
|
|
if let Some(tx) =
|
|
RpcNodeCore::pool(self).get_transaction_by_sender_and_nonce(sender, nonce)
|
|
{
|
|
let transaction = tx.transaction.clone_into_consensus();
|
|
return Ok(Some(from_recovered(transaction, self.tx_resp_builder())?));
|
|
}
|
|
}
|
|
|
|
// Check if the sender is a contract
|
|
if self.get_code(sender, None).await?.len() > 0 {
|
|
return Ok(None);
|
|
}
|
|
|
|
let highest = self.transaction_count(sender, None).await?.saturating_to::<u64>();
|
|
|
|
// If the nonce is higher or equal to the highest nonce, the transaction is pending or
|
|
// not exists.
|
|
if nonce >= highest {
|
|
return Ok(None);
|
|
}
|
|
|
|
let Ok(high) = self.provider().best_block_number() else {
|
|
return Err(EthApiError::HeaderNotFound(BlockNumberOrTag::Latest.into()).into());
|
|
};
|
|
|
|
// Perform a binary search over the block range to find the block in which the sender's
|
|
// nonce reached the requested nonce.
|
|
let num = binary_search::<_, _, Self::Error>(1, high, |mid| async move {
|
|
let mid_nonce =
|
|
self.transaction_count(sender, Some(mid.into())).await?.saturating_to::<u64>();
|
|
|
|
Ok(mid_nonce > nonce)
|
|
})
|
|
.await?;
|
|
|
|
let block_id = num.into();
|
|
self.block_with_senders(block_id)
|
|
.await?
|
|
.and_then(|block| {
|
|
let block_hash = block.hash();
|
|
let block_number = block.number();
|
|
let base_fee_per_gas = block.base_fee_per_gas();
|
|
|
|
block
|
|
.transactions_with_sender()
|
|
.enumerate()
|
|
.find(|(_, (signer, tx))| **signer == sender && (*tx).nonce() == nonce)
|
|
.map(|(index, (signer, tx))| {
|
|
let tx_info = TransactionInfo {
|
|
hash: Some(tx.hash()),
|
|
block_hash: Some(block_hash),
|
|
block_number: Some(block_number),
|
|
base_fee: base_fee_per_gas.map(u128::from),
|
|
index: Some(index as u64),
|
|
};
|
|
from_recovered_with_block_context(
|
|
tx.clone().with_signer(*signer),
|
|
tx_info,
|
|
self.tx_resp_builder(),
|
|
)
|
|
})
|
|
})
|
|
.ok_or(EthApiError::HeaderNotFound(block_id))?
|
|
.map(Some)
|
|
}
|
|
}
|
|
|
|
/// 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<Output = Result<Option<Bytes>, 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().get(index) {
|
|
return Ok(Some(tx.encoded_2718().into()))
|
|
}
|
|
}
|
|
|
|
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<Output = Result<B256, Self::Error>> + Send {
|
|
async move {
|
|
let recovered = recover_raw_transaction(tx)?;
|
|
let pool_transaction =
|
|
<Self::Pool as TransactionPool>::Transaction::from_pooled(recovered.into());
|
|
|
|
// 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<Output = Result<B256, Self::Error>> + Send
|
|
where
|
|
Self: EthApiSpec + LoadBlock + LoadPendingBlock + EstimateCall,
|
|
{
|
|
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.next_available_nonce(from).await?;
|
|
request.nonce = Some(nonce);
|
|
}
|
|
|
|
let chain_id = self.chain_id();
|
|
request.chain_id = Some(chain_id.to());
|
|
|
|
let estimated_gas =
|
|
self.estimate_gas_at(request.clone(), BlockId::pending(), None).await?;
|
|
let gas_limit = estimated_gas;
|
|
request.set_gas_limit(gas_limit.to());
|
|
|
|
let transaction = self.sign_request(&from, request).await?.with_signer(from);
|
|
|
|
let pool_transaction =
|
|
<<Self as RpcNodeCore>::Pool as TransactionPool>::Transaction::try_from_consensus(
|
|
transaction,
|
|
)
|
|
.map_err(|_| EthApiError::TransactionConversionError)?;
|
|
|
|
// 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 a transaction, with configured signers.
|
|
fn sign_request(
|
|
&self,
|
|
from: &Address,
|
|
txn: TransactionRequest,
|
|
) -> impl Future<Output = Result<TransactionSigned, Self::Error>> + Send {
|
|
async move {
|
|
self.find_signer(from)?
|
|
.sign_transaction(txn, from)
|
|
.await
|
|
.map_err(Self::Error::from_eth_err)
|
|
}
|
|
}
|
|
|
|
/// Signs given message. Returns the signature.
|
|
fn sign(
|
|
&self,
|
|
account: Address,
|
|
message: Bytes,
|
|
) -> impl Future<Output = Result<Bytes, Self::Error>> + Send {
|
|
async move {
|
|
Ok(self
|
|
.find_signer(&account)?
|
|
.sign(account, &message)
|
|
.await
|
|
.map_err(Self::Error::from_eth_err)?
|
|
.as_bytes()
|
|
.into())
|
|
}
|
|
}
|
|
|
|
/// Signs a transaction request using the given account in request
|
|
/// Returns the EIP-2718 encoded signed transaction.
|
|
fn sign_transaction(
|
|
&self,
|
|
request: TransactionRequest,
|
|
) -> impl Future<Output = Result<Bytes, Self::Error>> + Send {
|
|
async move {
|
|
let from = match request.from {
|
|
Some(from) => from,
|
|
None => return Err(SignError::NoAccount.into_eth_err()),
|
|
};
|
|
|
|
Ok(self.sign_request(&from, request).await?.encoded_2718().into())
|
|
}
|
|
}
|
|
|
|
/// Encodes and signs the typed data according EIP-712. Payload must implement Eip712 trait.
|
|
fn sign_typed_data(&self, data: &TypedData, account: Address) -> Result<Bytes, Self::Error> {
|
|
Ok(self
|
|
.find_signer(&account)?
|
|
.sign_typed_data(account, data)
|
|
.map_err(Self::Error::from_eth_err)?
|
|
.as_bytes()
|
|
.into())
|
|
}
|
|
|
|
/// Returns the signer for the given account, if found in configured signers.
|
|
fn find_signer(
|
|
&self,
|
|
account: &Address,
|
|
) -> Result<Box<(dyn EthSigner + 'static)>, 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
|
|
+ FullEthApiTypes
|
|
+ RpcNodeCoreExt<Provider: TransactionsProvider, Pool: TransactionPool>
|
|
{
|
|
/// Returns the transaction by hash.
|
|
///
|
|
/// Checks the pool and state.
|
|
///
|
|
/// Returns `Ok(None)` if no matching transaction was found.
|
|
#[expect(clippy::complexity)]
|
|
fn transaction_by_hash(
|
|
&self,
|
|
hash: B256,
|
|
) -> impl Future<
|
|
Output = Result<Option<TransactionSource<ProviderTx<Self::Provider>>>, 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_consensus())
|
|
{
|
|
resp = Some(TransactionSource::Pool(tx.into()));
|
|
}
|
|
}
|
|
|
|
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<Output = Result<Option<(TransactionSource, BlockId)>, 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<
|
|
Output = Result<Option<(TransactionSource, Arc<SealedBlockWithSenders>)>, 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_sealed_block_with_senders(block_hash)
|
|
.await
|
|
.map_err(Self::Error::from_eth_err)?;
|
|
Ok(block.map(|block| (transaction, block)))
|
|
}
|
|
}
|
|
}
|