Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rpc): implement eth_getTransactionBySenderAndNonce #10540

Merged
merged 8 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
jsvisa marked this conversation as resolved.
Show resolved Hide resolved

// 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));
}
}
Loading