Skip to content

Commit

Permalink
fix(fortuna): Reset nonce manager if receipt is not available (#1774)
Browse files Browse the repository at this point in the history
* Reset nonce manager if receipt is not available
  • Loading branch information
m30m authored Jul 18, 2024
1 parent 80a7c05 commit d927907
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 3 deletions.
4 changes: 3 additions & 1 deletion apps/fortuna/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion apps/fortuna/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "fortuna"
version = "6.4.1"
version = "6.4.2"
edition = "2021"

[dependencies]
Expand Down Expand Up @@ -37,6 +37,7 @@ url = "2.5.0"
chrono = { version = "0.4.38", features = ["clock", "std"] , default-features = false}
backoff = { version = "0.4.0", features = ["futures", "tokio"] }
thiserror = "1.0.61"
futures-locks = "0.7.1"


[dev-dependencies]
Expand Down
1 change: 1 addition & 0 deletions apps/fortuna/src/chain.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub(crate) mod eth_gas_oracle;
pub(crate) mod ethereum;
mod nonce_manager;
pub(crate) mod reader;
pub(crate) mod traced_client;
2 changes: 1 addition & 1 deletion apps/fortuna/src/chain/ethereum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use {
api::ChainId,
chain::{
eth_gas_oracle::EthProviderOracle,
nonce_manager::NonceManagerMiddleware,
reader::{
self,
BlockNumber,
Expand Down Expand Up @@ -33,7 +34,6 @@ use {
middleware::{
gas_oracle::GasOracleMiddleware,
MiddlewareError,
NonceManagerMiddleware,
SignerMiddleware,
},
prelude::{
Expand Down
199 changes: 199 additions & 0 deletions apps/fortuna/src/chain/nonce_manager.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
// This is a copy of the NonceManagerMiddleware from ethers-rs, with an additional reset method.
// Copied from: https://github.com/gakonst/ethers-rs/blob/34ed9e372e66235aed7074bc3f5c14922b139242/ethers-middleware/src/nonce_manager.rs

use {
axum::async_trait,
ethers::{
providers::{
Middleware,
MiddlewareError,
PendingTransaction,
},
types::{
transaction::eip2718::TypedTransaction,
*,
},
},
std::sync::atomic::{
AtomicBool,
AtomicU64,
Ordering,
},
thiserror::Error,
};

#[derive(Debug)]
/// Middleware used for calculating nonces locally, useful for signing multiple
/// consecutive transactions without waiting for them to hit the mempool
pub struct NonceManagerMiddleware<M> {
inner: M,
init_guard: futures_locks::Mutex<()>,
initialized: AtomicBool,
nonce: AtomicU64,
address: Address,
}

impl<M> NonceManagerMiddleware<M>
where
M: Middleware,
{
/// Instantiates the nonce manager with a 0 nonce. The `address` should be the
/// address which you'll be sending transactions from
pub fn new(inner: M, address: Address) -> Self {
Self {
inner,
init_guard: Default::default(),
initialized: Default::default(),
nonce: Default::default(),
address,
}
}

/// Returns the next nonce to be used
pub fn next(&self) -> U256 {
let nonce = self.nonce.fetch_add(1, Ordering::SeqCst);
nonce.into()
}

pub async fn initialize_nonce(
&self,
block: Option<BlockId>,
) -> Result<U256, NonceManagerError<M>> {
if self.initialized.load(Ordering::SeqCst) {
// return current nonce
return Ok(self.nonce.load(Ordering::SeqCst).into());
}

let _guard = self.init_guard.lock().await;

// do this again in case multiple tasks enter this codepath
if self.initialized.load(Ordering::SeqCst) {
// return current nonce
return Ok(self.nonce.load(Ordering::SeqCst).into());
}

// initialize the nonce the first time the manager is called
let nonce = self
.inner
.get_transaction_count(self.address, block)
.await
.map_err(MiddlewareError::from_err)?;
self.nonce.store(nonce.as_u64(), Ordering::SeqCst);
self.initialized.store(true, Ordering::SeqCst);
Ok(nonce)
} // guard dropped here

/// Resets the initialized flag so the next usage of the manager will reinitialize the nonce
/// based on the chain state.
/// This is useful when the RPC does not return an error if the transaction is submitted with
/// an incorrect nonce.
/// This is the only new method compared to the original NonceManagerMiddleware.
pub fn reset(&self) {
self.initialized.store(false, Ordering::SeqCst);
}

async fn get_transaction_count_with_manager(
&self,
block: Option<BlockId>,
) -> Result<U256, NonceManagerError<M>> {
// initialize the nonce the first time the manager is called
if !self.initialized.load(Ordering::SeqCst) {
let nonce = self
.inner
.get_transaction_count(self.address, block)
.await
.map_err(MiddlewareError::from_err)?;
self.nonce.store(nonce.as_u64(), Ordering::SeqCst);
self.initialized.store(true, Ordering::SeqCst);
}

Ok(self.next())
}
}

#[derive(Error, Debug)]
/// Thrown when an error happens at the Nonce Manager
pub enum NonceManagerError<M: Middleware> {
/// Thrown when the internal middleware errors
#[error("{0}")]
MiddlewareError(M::Error),
}

impl<M: Middleware> MiddlewareError for NonceManagerError<M> {
type Inner = M::Error;

fn from_err(src: M::Error) -> Self {
NonceManagerError::MiddlewareError(src)
}

fn as_inner(&self) -> Option<&Self::Inner> {
match self {
NonceManagerError::MiddlewareError(e) => Some(e),
}
}
}

#[async_trait]
impl<M> Middleware for NonceManagerMiddleware<M>
where
M: Middleware,
{
type Error = NonceManagerError<M>;
type Provider = M::Provider;
type Inner = M;

fn inner(&self) -> &M {
&self.inner
}

async fn fill_transaction(
&self,
tx: &mut TypedTransaction,
block: Option<BlockId>,
) -> Result<(), Self::Error> {
if tx.nonce().is_none() {
tx.set_nonce(self.get_transaction_count_with_manager(block).await?);
}

Ok(self
.inner()
.fill_transaction(tx, block)
.await
.map_err(MiddlewareError::from_err)?)
}

/// Signs and broadcasts the transaction. The optional parameter `block` can be passed so that
/// gas cost and nonce calculations take it into account. For simple transactions this can be
/// left to `None`.
async fn send_transaction<T: Into<TypedTransaction> + Send + Sync>(
&self,
tx: T,
block: Option<BlockId>,
) -> Result<PendingTransaction<'_, Self::Provider>, Self::Error> {
let mut tx = tx.into();

if tx.nonce().is_none() {
tx.set_nonce(self.get_transaction_count_with_manager(block).await?);
}

match self.inner.send_transaction(tx.clone(), block).await {
Ok(tx_hash) => Ok(tx_hash),
Err(err) => {
let nonce = self.get_transaction_count(self.address, block).await?;
if nonce != self.nonce.load(Ordering::SeqCst).into() {
// try re-submitting the transaction with the correct nonce if there
// was a nonce mismatch
self.nonce.store(nonce.as_u64(), Ordering::SeqCst);
tx.set_nonce(nonce);
self.inner
.send_transaction(tx, block)
.await
.map_err(MiddlewareError::from_err)
} else {
// propagate the error otherwise
Err(MiddlewareError::from_err(err))
}
}
}
}
}
4 changes: 4 additions & 0 deletions apps/fortuna/src/keeper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,10 @@ pub async fn process_event(
))
})?
.ok_or_else(|| {
// RPC may not return an error on tx submission if the nonce is too high.
// But we will never get a receipt. So we reset the nonce manager to get the correct nonce.
let nonce_manager = contract.client_ref().inner().inner();
nonce_manager.reset();
backoff::Error::transient(anyhow!(
"Can't verify the reveal, probably dropped from mempool Tx:{:?}",
transaction
Expand Down

0 comments on commit d927907

Please sign in to comment.