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

fix(fortuna): Reset nonce manager if receipt is not available #1774

Merged
merged 3 commits into from
Jul 18, 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
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
Loading