Skip to content

Commit

Permalink
feat(rpc): implement eth_getTransactionBySenderAndNonce (#10540)
Browse files Browse the repository at this point in the history
Signed-off-by: jsvisa <delweng@gmail.com>
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
  • Loading branch information
jsvisa and mattsse authored Aug 26, 2024
1 parent 4138b52 commit ec31b24
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 87 deletions.
76 changes: 72 additions & 4 deletions crates/rpc/rpc-eth-api/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,26 @@
use alloy_dyn_abi::TypedData;
use alloy_json_rpc::RpcObject;
use jsonrpsee::{core::RpcResult, proc_macros::rpc};
use jsonrpsee::{core::RpcResult, proc_macros::rpc, types::ErrorObjectOwned};
use reth_primitives::{
transaction::AccessListResult, Address, BlockId, BlockNumberOrTag, Bytes, B256, B64, U256, U64,
};
use reth_rpc_eth_types::{utils::binary_search, EthApiError};
use reth_rpc_server_types::{result::internal_rpc_err, ToRpcResult};
use reth_rpc_types::{
serde_helpers::JsonStorageKey,
simulate::{SimBlock, SimulatedBlock},
state::{EvmOverrides, StateOverride},
AnyTransactionReceipt, BlockOverrides, Bundle, EIP1186AccountProofResponse, EthCallResponse,
FeeHistory, Header, Index, StateContext, SyncStatus, TransactionRequest, Work,
AnyTransactionReceipt, BlockOverrides, BlockTransactions, Bundle, EIP1186AccountProofResponse,
EthCallResponse, FeeHistory, Header, Index, StateContext, SyncStatus, TransactionRequest, Work,
};
use reth_transaction_pool::{PoolTransaction, TransactionPool};
use tracing::trace;

use crate::{
helpers::{
transaction::UpdateRawTxForwarder, EthApiSpec, EthBlocks, EthCall, EthFees, EthState,
EthTransactions, FullEthApi,
EthTransactions, FullEthApi, LoadState,
},
RpcBlock, RpcTransaction,
};
Expand Down Expand Up @@ -164,6 +166,14 @@ pub trait EthApi<T: RpcObject, B: RpcObject> {
index: Index,
) -> RpcResult<Option<T>>;

/// Returns information about a transaction by sender and nonce.
#[method(name = "getTransactionBySenderAndNonce")]
async fn transaction_by_sender_and_nonce(
&self,
address: Address,
nonce: U64,
) -> RpcResult<Option<T>>;

/// Returns the receipt of a transaction by transaction hash.
#[method(name = "getTransactionReceipt")]
async fn transaction_receipt(&self, hash: B256) -> RpcResult<Option<AnyTransactionReceipt>>;
Expand Down Expand Up @@ -540,6 +550,64 @@ where
.await?)
}

/// Handler for: `eth_getTransactionBySenderAndNonce`
async fn transaction_by_sender_and_nonce(
&self,
sender: Address,
nonce: U64,
) -> RpcResult<Option<RpcTransaction<T::NetworkTypes>>> {
trace!(target: "rpc::eth", ?sender, ?nonce, "Serving eth_getTransactionBySenderAndNonce");
let nonce = nonce.to::<u64>();

// Check the pool first
if let Some(tx) = LoadState::pool(self).get_transaction_by_sender_and_nonce(sender, nonce) {
let transaction = tx.transaction.clone().into_consensus();
return Ok(Some(reth_rpc_types_compat::transaction::from_recovered(transaction)))
}

// Check if the sender is a contract
if self.get_code(sender, None).await?.len() > 0 {
return Ok(None)
}

let highest = EthState::transaction_count(self, 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)
}

// 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::<_, _, ErrorObjectOwned>(
1,
self.block_number()?.saturating_to(),
|mid| {
async move {
let mid_nonce = EthState::transaction_count(self, sender, Some(mid.into()))
.await?
.saturating_to::<u64>();

// The `transaction_count` returns the `nonce` after the transaction was
// executed, which is the state of the account after the block, and we need to
// find the transaction whose nonce is the pre-state, so
// need to compare with `nonce`(no equal).
Ok(mid_nonce > nonce)
}
},
)
.await?;

let Some(BlockTransactions::Full(transactions)) =
self.block_by_number(num.into(), true).await?.map(|block| block.transactions)
else {
return Err(EthApiError::UnknownBlockNumber.into());
};

Ok(transactions.into_iter().find(|tx| *tx.from == *sender && tx.nonce == nonce))
}

/// Handler for: `eth_getTransactionReceipt`
async fn transaction_receipt(&self, hash: B256) -> RpcResult<Option<AnyTransactionReceipt>> {
trace!(target: "rpc::eth", ?hash, "Serving eth_getTransactionReceipt");
Expand Down
2 changes: 1 addition & 1 deletion crates/rpc/rpc-eth-types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,4 @@ serde_json.workspace = true


[features]
js-tracer = ["revm-inspectors/js-tracer"]
js-tracer = ["revm-inspectors/js-tracer"]
65 changes: 65 additions & 0 deletions crates/rpc/rpc-eth-types/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Commonly used code snippets
use reth_primitives::{Bytes, PooledTransactionsElement, PooledTransactionsElementEcRecovered};
use std::future::Future;

use super::{EthApiError, EthResult};

Expand All @@ -17,3 +18,67 @@ pub fn recover_raw_transaction(data: Bytes) -> EthResult<PooledTransactionsEleme

transaction.try_into_ecrecovered().or(Err(EthApiError::InvalidTransactionSignature))
}

/// Performs a binary search within a given block range to find the desired block number.
///
/// The binary search is performed by calling the provided asynchronous `check` closure on the
/// blocks of the range. The closure should return a future representing the result of performing
/// the desired logic at a given block. The future resolves to an `bool` where:
/// - `true` indicates that the condition has been matched, but we can try to find a lower block to
/// make the condition more matchable.
/// - `false` indicates that the condition not matched, so the target is not present in the current
/// block and should continue searching in a higher range.
///
/// Args:
/// - `low`: The lower bound of the block range (inclusive).
/// - `high`: The upper bound of the block range (inclusive).
/// - `check`: A closure that performs the desired logic at a given block.
pub async fn binary_search<F, Fut, E>(low: u64, high: u64, check: F) -> Result<u64, E>
where
F: Fn(u64) -> Fut,
Fut: Future<Output = Result<bool, E>>,
{
let mut low = low;
let mut high = high;
let mut num = high;

while low <= high {
let mid = (low + high) / 2;
if check(mid).await? {
high = mid - 1;
num = mid;
} else {
low = mid + 1
}
}

Ok(num)
}

#[cfg(test)]
mod tests {
use super::*;

#[tokio::test]
async fn test_binary_search() {
// in the middle
let num: Result<_, ()> =
binary_search(1, 10, |mid| Box::pin(async move { Ok(mid >= 5) })).await;
assert_eq!(num, Ok(5));

// in the upper
let num: Result<_, ()> =
binary_search(1, 10, |mid| Box::pin(async move { Ok(mid >= 7) })).await;
assert_eq!(num, Ok(7));

// in the lower
let num: Result<_, ()> =
binary_search(1, 10, |mid| Box::pin(async move { Ok(mid >= 1) })).await;
assert_eq!(num, Ok(1));

// higher than the upper
let num: Result<_, ()> =
binary_search(1, 10, |mid| Box::pin(async move { Ok(mid >= 11) })).await;
assert_eq!(num, Ok(10));
}
}
111 changes: 29 additions & 82 deletions crates/rpc/rpc/src/otterscan.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use alloy_network::Network;
use alloy_primitives::Bytes;
use async_trait::async_trait;
use jsonrpsee::core::RpcResult;
use jsonrpsee::{core::RpcResult, types::ErrorObjectOwned};
use reth_primitives::{Address, BlockNumberOrTag, TxHash, B256, U256};
use reth_rpc_api::{EthApiServer, OtterscanServer};
use reth_rpc_eth_api::{helpers::TraceExt, EthApiTypes, RpcBlock, RpcTransaction};
use reth_rpc_eth_types::EthApiError;
use reth_rpc_eth_types::{utils::binary_search, EthApiError};
use reth_rpc_server_types::result::internal_rpc_err;
use reth_rpc_types::{
trace::{
Expand All @@ -22,7 +22,6 @@ use revm_inspectors::{
transfer::{TransferInspector, TransferKind},
};
use revm_primitives::ExecutionResult;
use std::future::Future;

const API_LEVEL: u64 = 8;

Expand Down Expand Up @@ -295,20 +294,24 @@ where

// 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(1, self.eth.block_number()?.saturating_to(), |mid| {
async move {
let mid_nonce =
EthApiServer::transaction_count(&self.eth, sender, Some(mid.into()))
.await?
.saturating_to::<u64>();

// The `transaction_count` returns the `nonce` after the transaction was
// executed, which is the state of the account after the block, and we need to find
// the transaction whose nonce is the pre-state, so need to compare with `nonce`(no
// equal).
Ok(mid_nonce > nonce)
}
})
let num = binary_search::<_, _, ErrorObjectOwned>(
1,
self.eth.block_number()?.saturating_to(),
|mid| {
async move {
let mid_nonce =
EthApiServer::transaction_count(&self.eth, sender, Some(mid.into()))
.await?
.saturating_to::<u64>();

// The `transaction_count` returns the `nonce` after the transaction was
// executed, which is the state of the account after the block, and we need to
// find the transaction whose nonce is the pre-state, so
// need to compare with `nonce`(no equal).
Ok(mid_nonce > nonce)
}
},
)
.await?;

let Some(BlockTransactions::Full(transactions)) =
Expand All @@ -329,11 +332,15 @@ where
return Ok(None);
}

let num = binary_search(1, self.eth.block_number()?.saturating_to(), |mid| {
Box::pin(
async move { Ok(!self.eth.get_code(address, Some(mid.into())).await?.is_empty()) },
)
})
let num = binary_search::<_, _, ErrorObjectOwned>(
1,
self.eth.block_number()?.saturating_to(),
|mid| {
Box::pin(async move {
Ok(!self.eth.get_code(address, Some(mid.into())).await?.is_empty())
})
},
)
.await?;

let traces = self
Expand Down Expand Up @@ -380,63 +387,3 @@ where
Ok(found)
}
}

/// Performs a binary search within a given block range to find the desired block number.
///
/// The binary search is performed by calling the provided asynchronous `check` closure on the
/// blocks of the range. The closure should return a future representing the result of performing
/// the desired logic at a given block. The future resolves to an `bool` where:
/// - `true` indicates that the condition has been matched, but we can try to find a lower block to
/// make the condition more matchable.
/// - `false` indicates that the condition not matched, so the target is not present in the current
/// block and should continue searching in a higher range.
///
/// Args:
/// - `low`: The lower bound of the block range (inclusive).
/// - `high`: The upper bound of the block range (inclusive).
/// - `check`: A closure that performs the desired logic at a given block.
async fn binary_search<F, Fut>(low: u64, high: u64, check: F) -> RpcResult<u64>
where
F: Fn(u64) -> Fut,
Fut: Future<Output = RpcResult<bool>>,
{
let mut low = low;
let mut high = high;
let mut num = high;

while low <= high {
let mid = (low + high) / 2;
if check(mid).await? {
high = mid - 1;
num = mid;
} else {
low = mid + 1
}
}

Ok(num)
}

#[cfg(test)]
mod tests {
use super::*;

#[tokio::test]
async fn test_binary_search() {
// in the middle
let num = binary_search(1, 10, |mid| Box::pin(async move { Ok(mid >= 5) })).await;
assert_eq!(num, Ok(5));

// in the upper
let num = binary_search(1, 10, |mid| Box::pin(async move { Ok(mid >= 7) })).await;
assert_eq!(num, Ok(7));

// in the lower
let num = binary_search(1, 10, |mid| Box::pin(async move { Ok(mid >= 1) })).await;
assert_eq!(num, Ok(1));

// high than the upper
let num = binary_search(1, 10, |mid| Box::pin(async move { Ok(mid >= 11) })).await;
assert_eq!(num, Ok(10));
}
}

0 comments on commit ec31b24

Please sign in to comment.