Skip to content

Commit

Permalink
feat(alloy-provider): add abstraction for NonceFiller behavior (all…
Browse files Browse the repository at this point in the history
…oy-rs#1108)

* feat(alloy-provider): add abstraction for `NonceFiller` behavior

* remove Default bound, add #[non_exhaustive]

* resolve conversations

* remove default generic

* fix ci
  • Loading branch information
StackOverflowExcept1on authored Aug 28, 2024
1 parent f6550c5 commit ec70d9c
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 60 deletions.
29 changes: 25 additions & 4 deletions crates/provider/src/builder.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::{
fillers::{
ChainIdFiller, FillerControlFlow, GasFiller, JoinFill, NonceFiller, RecommendedFiller,
TxFiller, WalletFiller,
CachedNonceManager, ChainIdFiller, FillerControlFlow, GasFiller, JoinFill, NonceFiller,
NonceManager, RecommendedFiller, SimpleNonceManager, TxFiller, WalletFiller,
},
provider::SendableTx,
Provider, RootProvider,
Expand Down Expand Up @@ -144,8 +144,29 @@ impl<L, N> ProviderBuilder<L, Identity, N> {
/// Add nonce management to the stack being built.
///
/// See [`NonceFiller`]
pub fn with_nonce_management(self) -> ProviderBuilder<L, JoinFill<Identity, NonceFiller>, N> {
self.filler(NonceFiller::default())
pub fn with_nonce_management<M: NonceManager>(
self,
nonce_manager: M,
) -> ProviderBuilder<L, JoinFill<Identity, NonceFiller<M>>, N> {
self.filler(NonceFiller::new(nonce_manager))
}

/// Add simple nonce management to the stack being built.
///
/// See [`SimpleNonceManager`]
pub fn with_simple_nonce_management(
self,
) -> ProviderBuilder<L, JoinFill<Identity, NonceFiller>, N> {
self.with_nonce_management(SimpleNonceManager::default())
}

/// Add cached nonce management to the stack being built.
///
/// See [`CachedNonceManager`]
pub fn with_cached_nonce_management(
self,
) -> ProviderBuilder<L, JoinFill<Identity, NonceFiller<CachedNonceManager>>, N> {
self.with_nonce_management(CachedNonceManager::default())
}

/// Add a chain ID filler to the stack being built. The filler will attempt
Expand Down
6 changes: 1 addition & 5 deletions crates/provider/src/fillers/gas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,11 +303,7 @@ mod tests {

#[tokio::test]
async fn non_eip1559_network() {
let provider = ProviderBuilder::new()
.filler(crate::fillers::GasFiller)
.filler(crate::fillers::NonceFiller::default())
.filler(crate::fillers::ChainIdFiller::default())
.on_anvil();
let provider = ProviderBuilder::new().with_recommended_fillers().on_anvil();

let tx = TransactionRequest {
from: Some(address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266")),
Expand Down
2 changes: 1 addition & 1 deletion crates/provider/src/fillers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ mod wallet;
pub use wallet::WalletFiller;

mod nonce;
pub use nonce::NonceFiller;
pub use nonce::{CachedNonceManager, NonceFiller, NonceManager, SimpleNonceManager};

mod gas;
pub use gas::{GasFillable, GasFiller};
Expand Down
159 changes: 109 additions & 50 deletions crates/provider/src/fillers/nonce.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,90 @@ use crate::{
use alloy_network::{Network, TransactionBuilder};
use alloy_primitives::Address;
use alloy_transport::{Transport, TransportResult};
use async_trait::async_trait;
use dashmap::DashMap;
use futures::lock::Mutex;
use std::sync::Arc;

/// A [`TxFiller`] that fills nonces on transactions.
/// A trait that determines the behavior of filling nonces.
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait NonceManager: Clone + Send + Sync + std::fmt::Debug {
/// Get the next nonce for the given account.
async fn get_next_nonce<P, T, N>(&self, provider: &P, address: Address) -> TransportResult<u64>
where
P: Provider<T, N>,
N: Network,
T: Transport + Clone;
}

/// This [`NonceManager`] implementation will fetch the transaction count for any new account it
/// sees.
///
/// Unlike [`CachedNonceManager`], this implementation does not store the transaction count locally,
/// which results in more frequent calls to the provider, but it is more resilient to chain
/// reorganizations.
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct SimpleNonceManager;

#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl NonceManager for SimpleNonceManager {
async fn get_next_nonce<P, T, N>(&self, provider: &P, address: Address) -> TransportResult<u64>
where
P: Provider<T, N>,
N: Network,
T: Transport + Clone,
{
provider.get_transaction_count(address).await
}
}

/// This [`NonceManager`] implementation will fetch the transaction count for any new account it
/// sees, store it locally and increment the locally stored nonce as transactions are sent via
/// [`Provider::send_transaction`].
///
/// The filler will fetch the transaction count for any new account it sees,
/// store it locally and increment the locally stored nonce as transactions are
/// sent via [`Provider::send_transaction`].
/// There is also an alternative implementation [`SimpleNonceManager`] that does not store the
/// transaction count locally.
#[derive(Clone, Debug, Default)]
pub struct CachedNonceManager {
nonces: DashMap<Address, Arc<Mutex<u64>>>,
}

#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl NonceManager for CachedNonceManager {
async fn get_next_nonce<P, T, N>(&self, provider: &P, address: Address) -> TransportResult<u64>
where
P: Provider<T, N>,
N: Network,
T: Transport + Clone,
{
// Use `u64::MAX` as a sentinel value to indicate that the nonce has not been fetched yet.
const NONE: u64 = u64::MAX;

// Locks dashmap internally for a short duration to clone the `Arc`.
// We also don't want to hold the dashmap lock through the await point below.
let nonce = {
let rm = self.nonces.entry(address).or_insert_with(|| Arc::new(Mutex::new(NONE)));
Arc::clone(rm.value())
};

let mut nonce = nonce.lock().await;
let new_nonce = if *nonce == NONE {
// Initialize the nonce if we haven't seen this account before.
provider.get_transaction_count(address).await?
} else {
*nonce + 1
};
*nonce = new_nonce;
Ok(new_nonce)
}
}

/// A [`TxFiller`] that fills nonces on transactions. The behavior of filling nonces is determined
/// by the [`NonceManager`].
///
/// # Note
///
Expand All @@ -31,7 +106,7 @@ use std::sync::Arc;
/// # use alloy_provider::{ProviderBuilder, RootProvider, Provider};
/// # async fn test<W: NetworkWallet<Ethereum> + Clone>(url: url::Url, wallet: W) -> Result<(), Box<dyn std::error::Error>> {
/// let provider = ProviderBuilder::new()
/// .with_nonce_management()
/// .with_simple_nonce_management()
/// .wallet(wallet)
/// .on_http(url);
///
Expand All @@ -40,11 +115,18 @@ use std::sync::Arc;
/// # }
/// ```
#[derive(Clone, Debug, Default)]
pub struct NonceFiller {
nonces: DashMap<Address, Arc<Mutex<u64>>>,
pub struct NonceFiller<M: NonceManager = SimpleNonceManager> {
nonce_manager: M,
}

impl<M: NonceManager> NonceFiller<M> {
/// Creates a new [`NonceFiller`] with the specified [`NonceManager`].
pub const fn new(nonce_manager: M) -> Self {
Self { nonce_manager }
}
}

impl<N: Network> TxFiller<N> for NonceFiller {
impl<M: NonceManager, N: Network> TxFiller<N> for NonceFiller<M> {
type Fillable = u64;

fn status(&self, tx: &<N as Network>::TransactionRequest) -> FillerControlFlow {
Expand All @@ -69,7 +151,7 @@ impl<N: Network> TxFiller<N> for NonceFiller {
T: Transport + Clone,
{
let from = tx.from().expect("checked by 'ready()'");
self.get_next_nonce(provider, from).await
self.nonce_manager.get_next_nonce(provider, from).await
}

async fn fill(
Expand All @@ -84,81 +166,58 @@ impl<N: Network> TxFiller<N> for NonceFiller {
}
}

impl NonceFiller {
/// Get the next nonce for the given account.
async fn get_next_nonce<P, T, N>(&self, provider: &P, address: Address) -> TransportResult<u64>
where
P: Provider<T, N>,
N: Network,
T: Transport + Clone,
{
// Use `u64::MAX` as a sentinel value to indicate that the nonce has not been fetched yet.
const NONE: u64 = u64::MAX;

// Locks dashmap internally for a short duration to clone the `Arc`.
// We also don't want to hold the dashmap lock through the await point below.
let nonce = {
let rm = self.nonces.entry(address).or_insert_with(|| Arc::new(Mutex::new(NONE)));
Arc::clone(rm.value())
};

let mut nonce = nonce.lock().await;
let new_nonce = if *nonce == NONE {
// Initialize the nonce if we haven't seen this account before.
provider.get_transaction_count(address).await?
} else {
*nonce + 1
};
*nonce = new_nonce;
Ok(new_nonce)
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::{ProviderBuilder, WalletProvider};
use alloy_primitives::{address, U256};
use alloy_rpc_types_eth::TransactionRequest;

async fn check_nonces<P, T, N>(filler: &NonceFiller, provider: &P, address: Address, start: u64)
where
async fn check_nonces<P, T, N, M>(
filler: &NonceFiller<M>,
provider: &P,
address: Address,
start: u64,
) where
P: Provider<T, N>,
N: Network,
T: Transport + Clone,
M: NonceManager,
{
for i in start..start + 5 {
let nonce = filler.get_next_nonce(&provider, address).await.unwrap();
let nonce = filler.nonce_manager.get_next_nonce(&provider, address).await.unwrap();
assert_eq!(nonce, i);
}
}

#[tokio::test]
async fn smoke_test() {
let filler = NonceFiller::default();
let filler = NonceFiller::<CachedNonceManager>::default();
let provider = ProviderBuilder::new().on_anvil();
let address = Address::ZERO;
check_nonces(&filler, &provider, address, 0).await;

#[cfg(feature = "anvil-api")]
{
use crate::ext::AnvilApi;
filler.nonces.clear();
filler.nonce_manager.nonces.clear();
provider.anvil_set_nonce(address, U256::from(69)).await.unwrap();
check_nonces(&filler, &provider, address, 69).await;
}
}

#[tokio::test]
async fn concurrency() {
let filler = Arc::new(NonceFiller::default());
let filler = Arc::new(NonceFiller::<CachedNonceManager>::default());
let provider = Arc::new(ProviderBuilder::new().on_anvil());
let address = Address::ZERO;
let tasks = (0..5)
.map(|_| {
let filler = Arc::clone(&filler);
let provider = Arc::clone(&provider);
tokio::spawn(async move { filler.get_next_nonce(&provider, address).await })
tokio::spawn(async move {
filler.nonce_manager.get_next_nonce(&provider, address).await
})
})
.collect::<Vec<_>>();

Expand All @@ -169,13 +228,13 @@ mod tests {
ns.sort_unstable();
assert_eq!(ns, (0..5).collect::<Vec<_>>());

assert_eq!(filler.nonces.len(), 1);
assert_eq!(*filler.nonces.get(&address).unwrap().value().lock().await, 4);
assert_eq!(filler.nonce_manager.nonces.len(), 1);
assert_eq!(*filler.nonce_manager.nonces.get(&address).unwrap().value().lock().await, 4);
}

#[tokio::test]
async fn no_nonce_if_sender_unset() {
let provider = ProviderBuilder::new().with_nonce_management().on_anvil();
let provider = ProviderBuilder::new().with_cached_nonce_management().on_anvil();

let tx = TransactionRequest {
value: Some(U256::from(100)),
Expand All @@ -191,7 +250,7 @@ mod tests {

#[tokio::test]
async fn increments_nonce() {
let provider = ProviderBuilder::new().with_nonce_management().on_anvil_with_wallet();
let provider = ProviderBuilder::new().with_cached_nonce_management().on_anvil_with_wallet();

let from = provider.default_signer_address();
let tx = TransactionRequest {
Expand Down

0 comments on commit ec70d9c

Please sign in to comment.