mirror of
https://github.com/hl-archive-node/nanoreth.git
synced 2025-12-06 10:59:55 +00:00
984 lines
36 KiB
Rust
984 lines
36 KiB
Rust
//! Contains RPC handler implementations specific to transactions
|
|
use crate::{
|
|
eth::{
|
|
api::pending_block::PendingBlockEnv,
|
|
error::{EthApiError, EthResult, SignError},
|
|
revm_utils::{
|
|
inspect, inspect_and_return_db, prepare_call_env, replay_transactions_until, transact,
|
|
EvmOverrides,
|
|
},
|
|
utils::recover_raw_transaction,
|
|
},
|
|
EthApi, EthApiSpec,
|
|
};
|
|
use async_trait::async_trait;
|
|
use reth_network_api::NetworkInfo;
|
|
use reth_primitives::{
|
|
eip4844::calc_blob_gasprice,
|
|
Address, BlockId, BlockNumberOrTag, Bytes, FromRecoveredPooledTransaction, Header,
|
|
IntoRecoveredTransaction, Receipt, SealedBlock,
|
|
TransactionKind::{Call, Create},
|
|
TransactionMeta, TransactionSigned, TransactionSignedEcRecovered, B256, U128, U256, U64,
|
|
};
|
|
use reth_provider::{
|
|
BlockReaderIdExt, ChainSpecProvider, EvmEnvProvider, StateProviderBox, StateProviderFactory,
|
|
};
|
|
use reth_revm::{
|
|
database::StateProviderDatabase,
|
|
env::{fill_block_env_with_coinbase, tx_env_with_recovered},
|
|
tracing::{TracingInspector, TracingInspectorConfig},
|
|
};
|
|
use reth_rpc_types::{
|
|
CallRequest, Index, Log, Transaction, TransactionInfo, TransactionReceipt, TransactionRequest,
|
|
TypedTransactionRequest,
|
|
};
|
|
use reth_rpc_types_compat::from_recovered_with_block_context;
|
|
use reth_transaction_pool::{TransactionOrigin, TransactionPool};
|
|
use revm::{
|
|
db::CacheDB,
|
|
primitives::{BlockEnv, CfgEnv},
|
|
Inspector,
|
|
};
|
|
use revm_primitives::{utilities::create_address, Env, ResultAndState, SpecId};
|
|
|
|
/// Helper alias type for the state's [CacheDB]
|
|
pub(crate) type StateCacheDB<'r> = CacheDB<StateProviderDatabase<StateProviderBox<'r>>>;
|
|
|
|
/// Commonly used transaction related functions for the [EthApi] type in the `eth_` namespace.
|
|
///
|
|
/// Async functions that are spawned onto the
|
|
/// [TracingCallPool](crate::tracing_call::TracingCallPool) begin with `spawn_`
|
|
#[async_trait::async_trait]
|
|
pub trait EthTransactions: Send + Sync {
|
|
/// Returns default gas limit to use for `eth_call` and tracing RPC methods.
|
|
fn call_gas_limit(&self) -> u64;
|
|
|
|
/// Returns the state at the given [BlockId]
|
|
fn state_at(&self, at: BlockId) -> EthResult<StateProviderBox<'_>>;
|
|
|
|
/// Executes the closure with the state that corresponds to the given [BlockId].
|
|
fn with_state_at_block<F, T>(&self, at: BlockId, f: F) -> EthResult<T>
|
|
where
|
|
F: FnOnce(StateProviderBox<'_>) -> EthResult<T>;
|
|
|
|
/// Executes the closure with the state that corresponds to the given [BlockId] on a new task
|
|
async fn spawn_with_state_at_block<F, T>(&self, at: BlockId, f: F) -> EthResult<T>
|
|
where
|
|
F: FnOnce(StateProviderBox<'_>) -> EthResult<T> + Send + 'static,
|
|
T: Send + 'static;
|
|
|
|
/// Returns the revm evm env for the requested [BlockId]
|
|
///
|
|
/// If the [BlockId] this will return the [BlockId::Hash] of the block the env was configured
|
|
/// for.
|
|
async fn evm_env_at(&self, at: BlockId) -> EthResult<(CfgEnv, BlockEnv, BlockId)>;
|
|
|
|
/// Returns the revm evm env for the raw block header
|
|
///
|
|
/// This is used for tracing raw blocks
|
|
async fn evm_env_for_raw_block(&self, at: &Header) -> EthResult<(CfgEnv, BlockEnv)>;
|
|
|
|
/// Get all transactions in the block with the given hash.
|
|
///
|
|
/// Returns `None` if block does not exist.
|
|
async fn transactions_by_block(&self, block: B256)
|
|
-> EthResult<Option<Vec<TransactionSigned>>>;
|
|
|
|
/// Get the entire block for the given id.
|
|
///
|
|
/// Returns `None` if block does not exist.
|
|
async fn block_by_id(&self, id: BlockId) -> EthResult<Option<SealedBlock>>;
|
|
|
|
/// Get all transactions in the block with the given hash.
|
|
///
|
|
/// Returns `None` if block does not exist.
|
|
async fn transactions_by_block_id(
|
|
&self,
|
|
block: BlockId,
|
|
) -> EthResult<Option<Vec<TransactionSigned>>>;
|
|
|
|
/// Returns the transaction by hash.
|
|
///
|
|
/// Checks the pool and state.
|
|
///
|
|
/// Returns `Ok(None)` if no matching transaction was found.
|
|
async fn transaction_by_hash(&self, hash: B256) -> EthResult<Option<TransactionSource>>;
|
|
|
|
/// Returns the transaction by including its corresponding [BlockId]
|
|
///
|
|
/// Note: this supports pending transactions
|
|
async fn transaction_by_hash_at(
|
|
&self,
|
|
hash: B256,
|
|
) -> EthResult<Option<(TransactionSource, BlockId)>>;
|
|
|
|
/// Returns the _historical_ transaction and the block it was mined in
|
|
async fn historical_transaction_by_hash_at(
|
|
&self,
|
|
hash: B256,
|
|
) -> EthResult<Option<(TransactionSource, B256)>>;
|
|
|
|
/// 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.
|
|
async fn transaction_receipt(&self, hash: B256) -> EthResult<Option<TransactionReceipt>>;
|
|
|
|
/// Decodes and recovers the transaction and submits it to the pool.
|
|
///
|
|
/// Returns the hash of the transaction.
|
|
async fn send_raw_transaction(&self, tx: Bytes) -> EthResult<B256>;
|
|
|
|
/// Signs transaction with a matching signer, if any and submits the transaction to the pool.
|
|
/// Returns the hash of the signed transaction.
|
|
async fn send_transaction(&self, request: TransactionRequest) -> EthResult<B256>;
|
|
|
|
/// Prepares the state and env for the given [CallRequest] at the given [BlockId] and executes
|
|
/// the closure on a new task returning the result of the closure.
|
|
async fn spawn_with_call_at<F, R>(
|
|
&self,
|
|
request: CallRequest,
|
|
at: BlockId,
|
|
overrides: EvmOverrides,
|
|
f: F,
|
|
) -> EthResult<R>
|
|
where
|
|
F: for<'r> FnOnce(StateCacheDB<'r>, Env) -> EthResult<R> + Send + 'static,
|
|
R: Send + 'static;
|
|
|
|
/// Executes the call request at the given [BlockId].
|
|
async fn transact_call_at(
|
|
&self,
|
|
request: CallRequest,
|
|
at: BlockId,
|
|
overrides: EvmOverrides,
|
|
) -> EthResult<(ResultAndState, Env)>;
|
|
|
|
/// Executes the call request at the given [BlockId] on a new task and returns the result of the
|
|
/// inspect call.
|
|
async fn spawn_inspect_call_at<I>(
|
|
&self,
|
|
request: CallRequest,
|
|
at: BlockId,
|
|
overrides: EvmOverrides,
|
|
inspector: I,
|
|
) -> EthResult<(ResultAndState, Env)>
|
|
where
|
|
I: for<'r> Inspector<StateCacheDB<'r>> + Send + 'static;
|
|
|
|
/// Executes the transaction on top of the given [BlockId] with a tracer configured by the
|
|
/// config.
|
|
///
|
|
/// The callback is then called with the [TracingInspector] and the [ResultAndState] after the
|
|
/// configured [Env] was inspected.
|
|
///
|
|
/// Caution: this is blocking
|
|
fn trace_at<F, R>(
|
|
&self,
|
|
env: Env,
|
|
config: TracingInspectorConfig,
|
|
at: BlockId,
|
|
f: F,
|
|
) -> EthResult<R>
|
|
where
|
|
F: FnOnce(TracingInspector, ResultAndState) -> EthResult<R>;
|
|
|
|
/// Same as [Self::trace_at] but also provides the used database to the callback.
|
|
///
|
|
/// Executes the transaction on top of the given [BlockId] with a tracer configured by the
|
|
/// config.
|
|
///
|
|
/// The callback is then called with the [TracingInspector] and the [ResultAndState] after the
|
|
/// configured [Env] was inspected.
|
|
async fn spawn_trace_at_with_state<F, R>(
|
|
&self,
|
|
env: Env,
|
|
config: TracingInspectorConfig,
|
|
at: BlockId,
|
|
f: F,
|
|
) -> EthResult<R>
|
|
where
|
|
F: for<'a> FnOnce(TracingInspector, ResultAndState, StateCacheDB<'a>) -> EthResult<R>
|
|
+ Send
|
|
+ 'static,
|
|
R: Send + 'static;
|
|
|
|
/// Fetches the transaction and the transaction's block
|
|
async fn transaction_and_block(
|
|
&self,
|
|
hash: B256,
|
|
) -> EthResult<Option<(TransactionSource, SealedBlock)>>;
|
|
|
|
/// Retrieves the transaction if it exists and returns its trace.
|
|
///
|
|
/// Before the transaction is traced, all previous transaction in the block are applied to the
|
|
/// state by executing them first.
|
|
/// The callback `f` is invoked with the [ResultAndState] after the transaction was executed and
|
|
/// the database that points to the beginning of the transaction.
|
|
///
|
|
/// Note: Implementers should use a threadpool where blocking is allowed, such as
|
|
/// [TracingCallPool](crate::tracing_call::TracingCallPool).
|
|
async fn spawn_trace_transaction_in_block<F, R>(
|
|
&self,
|
|
hash: B256,
|
|
config: TracingInspectorConfig,
|
|
f: F,
|
|
) -> EthResult<Option<R>>
|
|
where
|
|
F: for<'a> FnOnce(
|
|
TransactionInfo,
|
|
TracingInspector,
|
|
ResultAndState,
|
|
StateCacheDB<'a>,
|
|
) -> EthResult<R>
|
|
+ Send
|
|
+ 'static,
|
|
R: Send + 'static;
|
|
}
|
|
|
|
#[async_trait]
|
|
impl<Provider, Pool, Network> EthTransactions for EthApi<Provider, Pool, Network>
|
|
where
|
|
Pool: TransactionPool + Clone + 'static,
|
|
Provider:
|
|
BlockReaderIdExt + ChainSpecProvider + StateProviderFactory + EvmEnvProvider + 'static,
|
|
Network: NetworkInfo + Send + Sync + 'static,
|
|
{
|
|
fn call_gas_limit(&self) -> u64 {
|
|
self.inner.gas_cap
|
|
}
|
|
|
|
fn state_at(&self, at: BlockId) -> EthResult<StateProviderBox<'_>> {
|
|
self.state_at_block_id(at)
|
|
}
|
|
|
|
fn with_state_at_block<F, T>(&self, at: BlockId, f: F) -> EthResult<T>
|
|
where
|
|
F: FnOnce(StateProviderBox<'_>) -> EthResult<T>,
|
|
{
|
|
let state = self.state_at(at)?;
|
|
f(state)
|
|
}
|
|
|
|
async fn spawn_with_state_at_block<F, T>(&self, at: BlockId, f: F) -> EthResult<T>
|
|
where
|
|
F: FnOnce(StateProviderBox<'_>) -> EthResult<T> + Send + 'static,
|
|
T: Send + 'static,
|
|
{
|
|
let this = self.clone();
|
|
self.inner
|
|
.tracing_call_pool
|
|
.spawn(move || {
|
|
let state = this.state_at(at)?;
|
|
f(state)
|
|
})
|
|
.await
|
|
.map_err(|_| EthApiError::InternalTracingError)?
|
|
}
|
|
|
|
async fn evm_env_at(&self, at: BlockId) -> EthResult<(CfgEnv, BlockEnv, BlockId)> {
|
|
if at.is_pending() {
|
|
let PendingBlockEnv { cfg, block_env, origin } = self.pending_block_env_and_cfg()?;
|
|
Ok((cfg, block_env, origin.header().hash.into()))
|
|
} else {
|
|
// Use cached values if there is no pending block
|
|
let block_hash = self
|
|
.provider()
|
|
.block_hash_for_id(at)?
|
|
.ok_or_else(|| EthApiError::UnknownBlockNumber)?;
|
|
let (cfg, env) = self.cache().get_evm_env(block_hash).await?;
|
|
Ok((cfg, env, block_hash.into()))
|
|
}
|
|
}
|
|
|
|
async fn evm_env_for_raw_block(&self, header: &Header) -> EthResult<(CfgEnv, BlockEnv)> {
|
|
// get the parent config first
|
|
let (cfg, mut block_env, _) = self.evm_env_at(header.parent_hash.into()).await?;
|
|
|
|
let after_merge = cfg.spec_id >= SpecId::MERGE;
|
|
fill_block_env_with_coinbase(&mut block_env, header, after_merge, header.beneficiary);
|
|
|
|
Ok((cfg, block_env))
|
|
}
|
|
|
|
async fn transactions_by_block(
|
|
&self,
|
|
block: B256,
|
|
) -> EthResult<Option<Vec<TransactionSigned>>> {
|
|
Ok(self.cache().get_block_transactions(block).await?)
|
|
}
|
|
|
|
async fn block_by_id(&self, id: BlockId) -> EthResult<Option<SealedBlock>> {
|
|
self.block(id).await
|
|
}
|
|
|
|
async fn transactions_by_block_id(
|
|
&self,
|
|
block: BlockId,
|
|
) -> EthResult<Option<Vec<TransactionSigned>>> {
|
|
self.block_by_id(block).await.map(|block| block.map(|block| block.body))
|
|
}
|
|
|
|
async fn transaction_by_hash(&self, hash: B256) -> EthResult<Option<TransactionSource>> {
|
|
// Try to find the transaction on disk
|
|
let mut resp = self
|
|
.on_blocking_task(|this| async move {
|
|
match this.provider().transaction_by_hash_with_meta(hash)? {
|
|
None => Ok(None),
|
|
Some((tx, meta)) => {
|
|
let transaction = tx
|
|
.into_ecrecovered()
|
|
.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.to_recovered_transaction())
|
|
{
|
|
resp = Some(TransactionSource::Pool(tx));
|
|
}
|
|
}
|
|
|
|
Ok(resp)
|
|
}
|
|
|
|
async fn transaction_by_hash_at(
|
|
&self,
|
|
transaction_hash: B256,
|
|
) -> EthResult<Option<(TransactionSource, BlockId)>> {
|
|
match self.transaction_by_hash(transaction_hash).await? {
|
|
None => return Ok(None),
|
|
Some(tx) => {
|
|
let res = match tx {
|
|
tx @ TransactionSource::Pool(_) => {
|
|
(tx, BlockId::Number(BlockNumberOrTag::Pending))
|
|
}
|
|
TransactionSource::Block {
|
|
transaction,
|
|
index,
|
|
block_hash,
|
|
block_number,
|
|
base_fee,
|
|
} => {
|
|
let at = BlockId::Hash(block_hash.into());
|
|
let tx = TransactionSource::Block {
|
|
transaction,
|
|
index,
|
|
block_hash,
|
|
block_number,
|
|
base_fee,
|
|
};
|
|
(tx, at)
|
|
}
|
|
};
|
|
Ok(Some(res))
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn historical_transaction_by_hash_at(
|
|
&self,
|
|
hash: B256,
|
|
) -> EthResult<Option<(TransactionSource, B256)>> {
|
|
match self.transaction_by_hash_at(hash).await? {
|
|
None => Ok(None),
|
|
Some((tx, at)) => Ok(at.as_block_hash().map(|hash| (tx, hash))),
|
|
}
|
|
}
|
|
|
|
async fn transaction_receipt(&self, hash: B256) -> EthResult<Option<TransactionReceipt>> {
|
|
let result = self
|
|
.on_blocking_task(|this| async move {
|
|
let (tx, meta) = match this.provider().transaction_by_hash_with_meta(hash)? {
|
|
Some((tx, meta)) => (tx, meta),
|
|
None => return Ok(None),
|
|
};
|
|
|
|
let receipt = match this.provider().receipt_by_hash(hash)? {
|
|
Some(recpt) => recpt,
|
|
None => return Ok(None),
|
|
};
|
|
|
|
Ok(Some((tx, meta, receipt)))
|
|
})
|
|
.await?;
|
|
|
|
let (tx, meta, receipt) = match result {
|
|
Some((tx, meta, receipt)) => (tx, meta, receipt),
|
|
None => return Ok(None),
|
|
};
|
|
|
|
self.build_transaction_receipt(tx, meta, receipt).await.map(Some)
|
|
}
|
|
|
|
async fn send_raw_transaction(&self, tx: Bytes) -> EthResult<B256> {
|
|
let recovered = recover_raw_transaction(tx)?;
|
|
|
|
let pool_transaction = <Pool::Transaction>::from_recovered_transaction(recovered);
|
|
|
|
// submit the transaction to the pool with a `Local` origin
|
|
let hash = self.pool().add_transaction(TransactionOrigin::Local, pool_transaction).await?;
|
|
|
|
Ok(hash)
|
|
}
|
|
|
|
async fn send_transaction(&self, mut request: TransactionRequest) -> EthResult<B256> {
|
|
let from = match request.from {
|
|
Some(from) => from,
|
|
None => return Err(SignError::NoAccount.into()),
|
|
};
|
|
|
|
// set nonce if not already set before
|
|
if request.nonce.is_none() {
|
|
let nonce =
|
|
self.get_transaction_count(from, Some(BlockId::Number(BlockNumberOrTag::Pending)))?;
|
|
// note: `.to()` can't panic because the nonce is constructed from a `u64`
|
|
request.nonce = Some(U64::from(nonce.to::<u64>()));
|
|
}
|
|
|
|
let chain_id = self.chain_id();
|
|
// TODO: we need an oracle to fetch the gas price of the current chain
|
|
let gas_price = request.gas_price.unwrap_or_default();
|
|
let max_fee_per_gas = request.max_fee_per_gas.unwrap_or_default();
|
|
|
|
let estimated_gas = self
|
|
.estimate_gas_at(
|
|
CallRequest {
|
|
from: Some(from),
|
|
to: request.to,
|
|
gas: request.gas,
|
|
gas_price: Some(U256::from(gas_price)),
|
|
max_fee_per_gas: Some(U256::from(max_fee_per_gas)),
|
|
value: request.value,
|
|
input: request.data.clone().into(),
|
|
nonce: request.nonce,
|
|
chain_id: Some(chain_id),
|
|
access_list: request.access_list.clone(),
|
|
max_priority_fee_per_gas: Some(U256::from(max_fee_per_gas)),
|
|
transaction_type: None,
|
|
blob_versioned_hashes: None,
|
|
max_fee_per_blob_gas: None,
|
|
},
|
|
BlockId::Number(BlockNumberOrTag::Pending),
|
|
)
|
|
.await?;
|
|
let gas_limit = estimated_gas;
|
|
|
|
let transaction = match request.into_typed_request() {
|
|
Some(TypedTransactionRequest::Legacy(mut m)) => {
|
|
m.chain_id = Some(chain_id.to());
|
|
m.gas_limit = gas_limit;
|
|
m.gas_price = gas_price;
|
|
|
|
TypedTransactionRequest::Legacy(m)
|
|
}
|
|
Some(TypedTransactionRequest::EIP2930(mut m)) => {
|
|
m.chain_id = chain_id.to();
|
|
m.gas_limit = gas_limit;
|
|
m.gas_price = gas_price;
|
|
|
|
TypedTransactionRequest::EIP2930(m)
|
|
}
|
|
Some(TypedTransactionRequest::EIP1559(mut m)) => {
|
|
m.chain_id = chain_id.to();
|
|
m.gas_limit = gas_limit;
|
|
m.max_fee_per_gas = max_fee_per_gas;
|
|
|
|
TypedTransactionRequest::EIP1559(m)
|
|
}
|
|
None => return Err(EthApiError::ConflictingFeeFieldsInRequest),
|
|
};
|
|
|
|
let signed_tx = self.sign_request(&from, transaction)?;
|
|
|
|
let recovered =
|
|
signed_tx.into_ecrecovered().ok_or(EthApiError::InvalidTransactionSignature)?;
|
|
|
|
let pool_transaction = <Pool::Transaction>::from_recovered_transaction(recovered.into());
|
|
|
|
// submit the transaction to the pool with a `Local` origin
|
|
let hash = self.pool().add_transaction(TransactionOrigin::Local, pool_transaction).await?;
|
|
|
|
Ok(hash)
|
|
}
|
|
|
|
async fn spawn_with_call_at<F, R>(
|
|
&self,
|
|
request: CallRequest,
|
|
at: BlockId,
|
|
overrides: EvmOverrides,
|
|
f: F,
|
|
) -> EthResult<R>
|
|
where
|
|
F: for<'r> FnOnce(StateCacheDB<'r>, Env) -> EthResult<R> + Send + 'static,
|
|
R: Send + 'static,
|
|
{
|
|
let (cfg, block_env, at) = self.evm_env_at(at).await?;
|
|
let this = self.clone();
|
|
self.inner
|
|
.tracing_call_pool
|
|
.spawn(move || {
|
|
let state = this.state_at(at)?;
|
|
let mut db = CacheDB::new(StateProviderDatabase::new(state));
|
|
|
|
let env = prepare_call_env(
|
|
cfg,
|
|
block_env,
|
|
request,
|
|
this.call_gas_limit(),
|
|
&mut db,
|
|
overrides,
|
|
)?;
|
|
f(db, env)
|
|
})
|
|
.await
|
|
.map_err(|_| EthApiError::InternalTracingError)?
|
|
}
|
|
|
|
async fn transact_call_at(
|
|
&self,
|
|
request: CallRequest,
|
|
at: BlockId,
|
|
overrides: EvmOverrides,
|
|
) -> EthResult<(ResultAndState, Env)> {
|
|
self.spawn_with_call_at(request, at, overrides, move |mut db, env| transact(&mut db, env))
|
|
.await
|
|
}
|
|
|
|
async fn spawn_inspect_call_at<I>(
|
|
&self,
|
|
request: CallRequest,
|
|
at: BlockId,
|
|
overrides: EvmOverrides,
|
|
inspector: I,
|
|
) -> EthResult<(ResultAndState, Env)>
|
|
where
|
|
I: for<'r> Inspector<StateCacheDB<'r>> + Send + 'static,
|
|
{
|
|
self.spawn_with_call_at(request, at, overrides, move |db, env| inspect(db, env, inspector))
|
|
.await
|
|
}
|
|
|
|
fn trace_at<F, R>(
|
|
&self,
|
|
env: Env,
|
|
config: TracingInspectorConfig,
|
|
at: BlockId,
|
|
f: F,
|
|
) -> EthResult<R>
|
|
where
|
|
F: FnOnce(TracingInspector, ResultAndState) -> EthResult<R>,
|
|
{
|
|
self.with_state_at_block(at, |state| {
|
|
let db = CacheDB::new(StateProviderDatabase::new(state));
|
|
|
|
let mut inspector = TracingInspector::new(config);
|
|
let (res, _) = inspect(db, env, &mut inspector)?;
|
|
|
|
f(inspector, res)
|
|
})
|
|
}
|
|
|
|
async fn spawn_trace_at_with_state<F, R>(
|
|
&self,
|
|
env: Env,
|
|
config: TracingInspectorConfig,
|
|
at: BlockId,
|
|
f: F,
|
|
) -> EthResult<R>
|
|
where
|
|
F: for<'a> FnOnce(TracingInspector, ResultAndState, StateCacheDB<'a>) -> EthResult<R>
|
|
+ Send
|
|
+ 'static,
|
|
R: Send + 'static,
|
|
{
|
|
self.spawn_with_state_at_block(at, move |state| {
|
|
let db = CacheDB::new(StateProviderDatabase::new(state));
|
|
let mut inspector = TracingInspector::new(config);
|
|
let (res, _, db) = inspect_and_return_db(db, env, &mut inspector)?;
|
|
|
|
f(inspector, res, db)
|
|
})
|
|
.await
|
|
}
|
|
|
|
async fn transaction_and_block(
|
|
&self,
|
|
hash: B256,
|
|
) -> EthResult<Option<(TransactionSource, SealedBlock)>> {
|
|
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(block_hash).await?;
|
|
Ok(block.map(|block| (transaction, block.seal(block_hash))))
|
|
}
|
|
|
|
async fn spawn_trace_transaction_in_block<F, R>(
|
|
&self,
|
|
hash: B256,
|
|
config: TracingInspectorConfig,
|
|
f: F,
|
|
) -> EthResult<Option<R>>
|
|
where
|
|
F: for<'a> FnOnce(
|
|
TransactionInfo,
|
|
TracingInspector,
|
|
ResultAndState,
|
|
StateCacheDB<'a>,
|
|
) -> EthResult<R>
|
|
+ Send
|
|
+ 'static,
|
|
R: Send + 'static,
|
|
{
|
|
let (transaction, block) = match self.transaction_and_block(hash).await? {
|
|
None => return Ok(None),
|
|
Some(res) => res,
|
|
};
|
|
let (tx, tx_info) = transaction.split();
|
|
|
|
let (cfg, block_env, _) = self.evm_env_at(block.hash.into()).await?;
|
|
|
|
// we need to get the state of the parent block because we're essentially replaying the
|
|
// block the transaction is included in
|
|
let parent_block = block.parent_hash;
|
|
let block_txs = block.body;
|
|
|
|
self.spawn_with_state_at_block(parent_block.into(), move |state| {
|
|
let mut db = CacheDB::new(StateProviderDatabase::new(state));
|
|
|
|
// replay all transactions prior to the targeted transaction
|
|
replay_transactions_until(&mut db, cfg.clone(), block_env.clone(), block_txs, tx.hash)?;
|
|
|
|
let env = Env { cfg, block: block_env, tx: tx_env_with_recovered(&tx) };
|
|
|
|
let mut inspector = TracingInspector::new(config);
|
|
let (res, _, db) = inspect_and_return_db(db, env, &mut inspector)?;
|
|
f(tx_info, inspector, res, db)
|
|
})
|
|
.await
|
|
.map(Some)
|
|
}
|
|
}
|
|
|
|
// === impl EthApi ===
|
|
|
|
impl<Provider, Pool, Network> EthApi<Provider, Pool, Network>
|
|
where
|
|
Provider:
|
|
BlockReaderIdExt + ChainSpecProvider + StateProviderFactory + EvmEnvProvider + 'static,
|
|
Network: 'static,
|
|
{
|
|
/// Helper function for `eth_getTransactionReceipt`
|
|
///
|
|
/// Returns the receipt
|
|
pub(crate) async fn build_transaction_receipt(
|
|
&self,
|
|
tx: TransactionSigned,
|
|
meta: TransactionMeta,
|
|
receipt: Receipt,
|
|
) -> EthResult<TransactionReceipt> {
|
|
// get all receipts for the block
|
|
let all_receipts = match self.cache().get_receipts(meta.block_hash).await? {
|
|
Some(recpts) => recpts,
|
|
None => return Err(EthApiError::UnknownBlockNumber),
|
|
};
|
|
build_transaction_receipt_with_block_receipts(tx, meta, receipt, &all_receipts)
|
|
}
|
|
}
|
|
|
|
impl<Provider, Pool, Network> EthApi<Provider, Pool, Network>
|
|
where
|
|
Pool: TransactionPool + 'static,
|
|
Provider:
|
|
BlockReaderIdExt + ChainSpecProvider + StateProviderFactory + EvmEnvProvider + 'static,
|
|
Network: NetworkInfo + Send + Sync + 'static,
|
|
{
|
|
pub(crate) fn sign_request(
|
|
&self,
|
|
from: &Address,
|
|
request: TypedTransactionRequest,
|
|
) -> EthResult<TransactionSigned> {
|
|
for signer in self.inner.signers.iter() {
|
|
if signer.is_signer_for(from) {
|
|
return match signer.sign_transaction(request, from) {
|
|
Ok(tx) => Ok(tx),
|
|
Err(e) => Err(e.into()),
|
|
}
|
|
}
|
|
}
|
|
Err(EthApiError::InvalidTransactionSignature)
|
|
}
|
|
|
|
/// Get Transaction by [BlockId] and the index of the transaction within that Block.
|
|
///
|
|
/// Returns `Ok(None)` if the block does not exist, or the block as fewer transactions
|
|
pub(crate) async fn transaction_by_block_and_tx_index(
|
|
&self,
|
|
block_id: impl Into<BlockId>,
|
|
index: Index,
|
|
) -> EthResult<Option<Transaction>> {
|
|
let block_id = block_id.into();
|
|
|
|
if let Some(block) = self.block(block_id).await? {
|
|
let block_hash = block.hash;
|
|
let block = block.unseal();
|
|
if let Some(tx_signed) = block.body.into_iter().nth(index.into()) {
|
|
let tx =
|
|
tx_signed.into_ecrecovered().ok_or(EthApiError::InvalidTransactionSignature)?;
|
|
return Ok(Some(from_recovered_with_block_context(
|
|
tx,
|
|
block_hash,
|
|
block.header.number,
|
|
block.header.base_fee_per_gas,
|
|
index.into(),
|
|
)))
|
|
}
|
|
}
|
|
|
|
Ok(None)
|
|
}
|
|
}
|
|
/// Represents from where a transaction was fetched.
|
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
|
pub enum TransactionSource {
|
|
/// Transaction exists in the pool (Pending)
|
|
Pool(TransactionSignedEcRecovered),
|
|
/// Transaction already included in a block
|
|
///
|
|
/// This can be a historical block or a pending block (received from the CL)
|
|
Block {
|
|
/// Transaction fetched via provider
|
|
transaction: TransactionSignedEcRecovered,
|
|
/// Index of the transaction in the block
|
|
index: u64,
|
|
/// Hash of the block.
|
|
block_hash: B256,
|
|
/// Number of the block.
|
|
block_number: u64,
|
|
/// base fee of the block.
|
|
base_fee: Option<u64>,
|
|
},
|
|
}
|
|
|
|
// === impl TransactionSource ===
|
|
|
|
impl TransactionSource {
|
|
/// Consumes the type and returns the wrapped transaction.
|
|
pub fn into_recovered(self) -> TransactionSignedEcRecovered {
|
|
self.into()
|
|
}
|
|
|
|
/// Returns the transaction and block related info, if not pending
|
|
pub fn split(self) -> (TransactionSignedEcRecovered, TransactionInfo) {
|
|
match self {
|
|
TransactionSource::Pool(tx) => {
|
|
let hash = tx.hash();
|
|
(
|
|
tx,
|
|
TransactionInfo {
|
|
hash: Some(hash),
|
|
index: None,
|
|
block_hash: None,
|
|
block_number: None,
|
|
base_fee: None,
|
|
},
|
|
)
|
|
}
|
|
TransactionSource::Block { transaction, index, block_hash, block_number, base_fee } => {
|
|
let hash = transaction.hash();
|
|
(
|
|
transaction,
|
|
TransactionInfo {
|
|
hash: Some(hash),
|
|
index: Some(index),
|
|
block_hash: Some(block_hash),
|
|
block_number: Some(block_number),
|
|
base_fee,
|
|
},
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<TransactionSource> for TransactionSignedEcRecovered {
|
|
fn from(value: TransactionSource) -> Self {
|
|
match value {
|
|
TransactionSource::Pool(tx) => tx,
|
|
TransactionSource::Block { transaction, .. } => transaction,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<TransactionSource> for Transaction {
|
|
fn from(value: TransactionSource) -> Self {
|
|
match value {
|
|
TransactionSource::Pool(tx) => reth_rpc_types_compat::transaction::from_recovered(tx),
|
|
TransactionSource::Block { transaction, index, block_hash, block_number, base_fee } => {
|
|
from_recovered_with_block_context(
|
|
transaction,
|
|
block_hash,
|
|
block_number,
|
|
base_fee,
|
|
U256::from(index),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Helper function to construct a transaction receipt
|
|
pub(crate) fn build_transaction_receipt_with_block_receipts(
|
|
tx: TransactionSigned,
|
|
meta: TransactionMeta,
|
|
receipt: Receipt,
|
|
all_receipts: &[Receipt],
|
|
) -> EthResult<TransactionReceipt> {
|
|
let transaction =
|
|
tx.clone().into_ecrecovered().ok_or(EthApiError::InvalidTransactionSignature)?;
|
|
|
|
// get the previous transaction cumulative gas used
|
|
let gas_used = if meta.index == 0 {
|
|
receipt.cumulative_gas_used
|
|
} else {
|
|
let prev_tx_idx = (meta.index - 1) as usize;
|
|
all_receipts
|
|
.get(prev_tx_idx)
|
|
.map(|prev_receipt| receipt.cumulative_gas_used - prev_receipt.cumulative_gas_used)
|
|
.unwrap_or_default()
|
|
};
|
|
|
|
let mut res_receipt = TransactionReceipt {
|
|
transaction_hash: Some(meta.tx_hash),
|
|
transaction_index: U64::from(meta.index),
|
|
block_hash: Some(meta.block_hash),
|
|
block_number: Some(U256::from(meta.block_number)),
|
|
from: transaction.signer(),
|
|
to: None,
|
|
cumulative_gas_used: U256::from(receipt.cumulative_gas_used),
|
|
gas_used: Some(U256::from(gas_used)),
|
|
contract_address: None,
|
|
logs: Vec::with_capacity(receipt.logs.len()),
|
|
effective_gas_price: U128::from(transaction.effective_gas_price(meta.base_fee)),
|
|
transaction_type: tx.transaction.tx_type().into(),
|
|
// TODO pre-byzantium receipts have a post-transaction state root
|
|
state_root: None,
|
|
logs_bloom: receipt.bloom_slow(),
|
|
status_code: if receipt.success { Some(U64::from(1)) } else { Some(U64::from(0)) },
|
|
|
|
// EIP-4844 fields
|
|
blob_gas_price: meta.excess_blob_gas.map(calc_blob_gasprice).map(U128::from),
|
|
blob_gas_used: transaction.transaction.blob_gas_used().map(U128::from),
|
|
};
|
|
|
|
match tx.transaction.kind() {
|
|
Create => {
|
|
res_receipt.contract_address =
|
|
Some(create_address(transaction.signer(), tx.transaction.nonce()));
|
|
}
|
|
Call(addr) => {
|
|
res_receipt.to = Some(*addr);
|
|
}
|
|
}
|
|
|
|
// get number of logs in the block
|
|
let mut num_logs = 0;
|
|
for prev_receipt in all_receipts.iter().take(meta.index as usize) {
|
|
num_logs += prev_receipt.logs.len();
|
|
}
|
|
|
|
for (tx_log_idx, log) in receipt.logs.into_iter().enumerate() {
|
|
let rpclog = Log {
|
|
address: log.address,
|
|
topics: log.topics,
|
|
data: log.data,
|
|
block_hash: Some(meta.block_hash),
|
|
block_number: Some(U256::from(meta.block_number)),
|
|
transaction_hash: Some(meta.tx_hash),
|
|
transaction_index: Some(U256::from(meta.index)),
|
|
log_index: Some(U256::from(num_logs + tx_log_idx)),
|
|
removed: false,
|
|
};
|
|
res_receipt.logs.push(rpclog);
|
|
}
|
|
|
|
Ok(res_receipt)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::{
|
|
eth::{cache::EthStateCache, gas_oracle::GasPriceOracle},
|
|
EthApi, TracingCallPool,
|
|
};
|
|
use reth_network_api::noop::NoopNetwork;
|
|
use reth_primitives::{constants::ETHEREUM_BLOCK_GAS_LIMIT, hex_literal::hex, Bytes};
|
|
use reth_provider::test_utils::NoopProvider;
|
|
use reth_transaction_pool::{test_utils::testing_pool, TransactionPool};
|
|
|
|
#[tokio::test]
|
|
async fn send_raw_transaction() {
|
|
let noop_provider = NoopProvider::default();
|
|
let noop_network_provider = NoopNetwork::default();
|
|
|
|
let pool = testing_pool();
|
|
|
|
let cache = EthStateCache::spawn(noop_provider, Default::default());
|
|
let eth_api = EthApi::new(
|
|
noop_provider,
|
|
pool.clone(),
|
|
noop_network_provider,
|
|
cache.clone(),
|
|
GasPriceOracle::new(noop_provider, Default::default(), cache),
|
|
ETHEREUM_BLOCK_GAS_LIMIT,
|
|
TracingCallPool::build().expect("failed to build tracing pool"),
|
|
);
|
|
|
|
// https://etherscan.io/tx/0xa694b71e6c128a2ed8e2e0f6770bddbe52e3bb8f10e8472f9a79ab81497a8b5d
|
|
let tx_1 = Bytes::from(hex!("02f871018303579880850555633d1b82520894eee27662c2b8eba3cd936a23f039f3189633e4c887ad591c62bdaeb180c080a07ea72c68abfb8fca1bd964f0f99132ed9280261bdca3e549546c0205e800f7d0a05b4ef3039e9c9b9babc179a1878fb825b5aaf5aed2fa8744854150157b08d6f3"));
|
|
|
|
let tx_1_result = eth_api.send_raw_transaction(tx_1).await.unwrap();
|
|
assert_eq!(
|
|
pool.len(),
|
|
1,
|
|
"expect 1 transactions in the pool, but pool size is {}",
|
|
pool.len()
|
|
);
|
|
|
|
// https://etherscan.io/tx/0x48816c2f32c29d152b0d86ff706f39869e6c1f01dc2fe59a3c1f9ecf39384694
|
|
let tx_2 = Bytes::from(hex!("02f9043c018202b7843b9aca00850c807d37a08304d21d94ef1c6e67703c7bd7107eed8303fbe6ec2554bf6b881bc16d674ec80000b903c43593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000063e2d99f00000000000000000000000000000000000000000000000000000000000000030b000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000001bc16d674ec80000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000065717fe021ea67801d1088cc80099004b05b64600000000000000000000000000000000000000000000000001bc16d674ec80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009e95fd5965fd1f1a6f0d4600000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000428dca9537116148616a5a3e44035af17238fe9dc080a0c6ec1e41f5c0b9511c49b171ad4e04c6bb419c74d99fe9891d74126ec6e4e879a032069a753d7a2cfa158df95421724d24c0e9501593c09905abf3699b4a4405ce"));
|
|
|
|
let tx_2_result = eth_api.send_raw_transaction(tx_2).await.unwrap();
|
|
assert_eq!(
|
|
pool.len(),
|
|
2,
|
|
"expect 2 transactions in the pool, but pool size is {}",
|
|
pool.len()
|
|
);
|
|
|
|
assert!(pool.get(&tx_1_result).is_some(), "tx1 not found in the pool");
|
|
assert!(pool.get(&tx_2_result).is_some(), "tx2 not found in the pool");
|
|
}
|
|
}
|