فهرست منبع

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

* Reset nonce manager if receipt is not available
Amin Moghaddam 1 سال پیش
والد
کامیت
d927907cae

+ 3 - 1
apps/fortuna/Cargo.lock

@@ -1502,7 +1502,7 @@ dependencies = [
 
 [[package]]
 name = "fortuna"
-version = "6.4.1"
+version = "6.4.2"
 dependencies = [
  "anyhow",
  "axum",
@@ -1517,6 +1517,7 @@ dependencies = [
  "ethabi",
  "ethers",
  "futures",
+ "futures-locks",
  "hex",
  "lazy_static",
  "once_cell",
@@ -1612,6 +1613,7 @@ checksum = "45ec6fe3675af967e67c5536c0b9d44e34e6c52f86bedc4ea49c5317b8e94d06"
 dependencies = [
  "futures-channel",
  "futures-task",
+ "tokio",
 ]
 
 [[package]]

+ 2 - 1
apps/fortuna/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 name    = "fortuna"
-version = "6.4.1"
+version = "6.4.2"
 edition = "2021"
 
 [dependencies]
@@ -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]

+ 1 - 0
apps/fortuna/src/chain.rs

@@ -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;

+ 1 - 1
apps/fortuna/src/chain/ethereum.rs

@@ -3,6 +3,7 @@ use {
         api::ChainId,
         chain::{
             eth_gas_oracle::EthProviderOracle,
+            nonce_manager::NonceManagerMiddleware,
             reader::{
                 self,
                 BlockNumber,
@@ -33,7 +34,6 @@ use {
         middleware::{
             gas_oracle::GasOracleMiddleware,
             MiddlewareError,
-            NonceManagerMiddleware,
             SignerMiddleware,
         },
         prelude::{

+ 199 - 0
apps/fortuna/src/chain/nonce_manager.rs

@@ -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 - 0
apps/fortuna/src/keeper.rs

@@ -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