Repository: Cardinal-Cryptography/zkOS-monorepo
Pull request #188: Backoffs
Author: login: fbielejec
Created: 2025-03-26T19:25:41Z | Merged: 2025-03-28T10:43:00Z



Diff:
diff --git a/crates/ar-cli/src/collect_viewing_keys.rs b/crates/ar-cli/src/collect_viewing_keys.rs
index b4ccfb56..1bbaf318 100644
--- a/crates/ar-cli/src/collect_viewing_keys.rs
+++ b/crates/ar-cli/src/collect_viewing_keys.rs
@@ -5,54 +5,25 @@ use std::{
     path::PathBuf,
 };
 
-use alloy_json_rpc::RpcError;
 use alloy_network::AnyNetwork;
 use alloy_primitives::{Address, U256};
 use alloy_provider::{Provider, ProviderBuilder};
 use alloy_rpc_types::{BlockNumberOrTag, BlockTransactionsKind, TransactionTrait};
 use alloy_sol_types::SolCall;
-use alloy_transport::TransportErrorKind;
-use hex::FromHexError;
 use log::{debug, info, trace};
 use rusqlite::Connection;
 use shielder_circuits::{grumpkin, GrumpkinPointAffine};
-use shielder_contract::{
-    ShielderContract::{newAccountERC20Call, newAccountNativeCall},
-    ShielderContractError,
-};
-use thiserror::Error;
+use shielder_contract::ShielderContract::{newAccountERC20Call, newAccountNativeCall};
 use type_conversions::u256_to_field;
 
 use crate::{
     cli::Endianess,
     db::{self, ViewingKey},
+    recoverable_error::MaybeRecoverableError,
 };
 
 const CHECKPOINT_TABLE_NAME: &str = "last_keys_block";
 
-#[derive(Debug, Error)]
-#[error(transparent)]
-#[non_exhaustive]
-pub enum CollectKeysError {
-    #[error("Error while interacting with the Shielder contract")]
-    Contract(#[from] ShielderContractError),
-
-    #[error("RPC error")]
-    Rpc(#[from] RpcError<TransportErrorKind>),
-
-    #[error("Hex decoding error")]
-    HexError(#[from] FromHexError),
-
-    #[error("Error reading AR private key file")]
-    ARKeyRead(#[from] std::io::Error),
-
-    #[error("Error converting from a little-endian byte representation to grumpkin::Fr")]
-    NotAGrumpkinBaseFieldElement,
-
-    #[error("Error while persisting data")]
-    Db(#[from] rusqlite::Error),
-}
-
 /// Goes back in transaction history and collects all viewing keys
 /// - look for new_account txs
 /// - read c1,c2
@@ -64,9 +35,11 @@ pub async fn run(
     private_key_file: &PathBuf,
     endianess: Endianess,
     from_block: u64,
-    connection: Connection,
+    db_path: &PathBuf,
     redact_sensitive_data: bool,
-) -> Result<(), CollectKeysError> {
+) -> Result<(), MaybeRecoverableError> {
+    let connection = db::init(db_path)?;
+
     let provider = ProviderBuilder::new()
         .network::<AnyNetwork>()
         .on_builtin(rpc_url)
@@ -86,7 +59,7 @@ pub async fn run(
 
     let private_key = grumpkin::Fr::from_bytes(&bytes)
         .into_option()
-        .ok_or(CollectKeysError::NotAGrumpkinBaseFieldElement)?;
+        .ok_or(MaybeRecoverableError::NotAGrumpkinBaseFieldElement)?;
 
     db::create_viewing_keys_table(&connection)?;
     db::create_checkpoint_table(&connection, CHECKPOINT_TABLE_NAME)?;
@@ -155,8 +128,6 @@ pub async fn run(
         db::update_checkpoint(&connection, CHECKPOINT_TABLE_NAME, block_number)?;
     }
 
-    info!("Done");
-
     Ok(())
 }
 
@@ -168,7 +139,7 @@ fn decode_and_persist(
     c2y: U256,
     private_key: grumpkin::Fr,
     redact_sensitive_data: bool,
-) -> Result<(), CollectKeysError> {
+) -> Result<(), MaybeRecoverableError> {
     let ciphertext1 = GrumpkinPointAffine::new(u256_to_field(c1x), u256_to_field(c1y));
     let ciphertext2 = GrumpkinPointAffine::new(u256_to_field(c2x), u256_to_field(c2y));
 
diff --git a/crates/ar-cli/src/index_events.rs b/crates/ar-cli/src/index_events.rs
index 8ff8e122..069a5024 100644
--- a/crates/ar-cli/src/index_events.rs
+++ b/crates/ar-cli/src/index_events.rs
@@ -1,53 +1,36 @@
-use std::cmp::{max, min};
+use std::{
+    cmp::{max, min},
+    path::PathBuf,
+};
 
-use alloy_json_rpc::RpcError;
 use alloy_primitives::Address;
 use alloy_provider::Provider;
 use alloy_rpc_types::{Filter, Log};
 use alloy_sol_types::SolEvent;
-use alloy_transport::TransportErrorKind;
 use log::{debug, info, trace};
 use rusqlite::Connection;
 use shielder_circuits::Fr;
 use shielder_contract::{
     providers::create_simple_provider,
     ShielderContract::{Deposit, NewAccount, ShielderContractEvents, Withdraw},
-    ShielderContractError,
 };
-use thiserror::Error;
 use type_conversions::u256_to_field;
 
-use crate::db::{self, Event};
+use crate::{
+    db::{self, Event},
+    recoverable_error::MaybeRecoverableError,
+};
 
 const CHECKPOINT_TABLE_NAME: &str = "last_events_block";
 
-#[derive(Debug, Error)]
-#[error(transparent)]
-#[non_exhaustive]
-pub enum IndexEventsError {
-    #[error("Error while interacting with the Shielder contract")]
-    Contract(#[from] ShielderContractError),
-
-    #[error("RPC error")]
-    Rpc(#[from] RpcError<TransportErrorKind>),
-
-    #[error("Error while decoding event log")]
-    EventLog(#[from] alloy_sol_types::Error),
-
-    #[error("Event is missing some data")]
-    MissingData,
-
-    #[error("Error while persisting data")]
-    Db(#[from] rusqlite::Error),
-}
-
 pub async fn run(
     rpc_url: &str,
     shielder_address: &Address,
     from_block: u64,
     batch_size: usize,
-    connection: Connection,
-) -> Result<(), IndexEventsError> {
+    db_path: &PathBuf,
+) -> Result<(), MaybeRecoverableError> {
+    let connection = db::init(db_path)?;
     let provider = create_simple_provider(rpc_url).await?;
     let current_height = provider.get_block_number().await?;
     let base_filter = Filter::new().address(*shielder_address);
@@ -80,10 +63,13 @@ pub async fn run(
     Ok(())
 }
 
-fn process_logs(logs: Vec<Log>, connection: &Connection) -> Result<(), IndexEventsError> {
+fn process_logs(logs: Vec<Log>, connection: &Connection) -> Result<(), MaybeRecoverableError> {
     for log in logs {
-        let tx_hash = log.transaction_hash.ok_or(IndexEventsError::MissingData)?.0;
-        let block_number = log.block_number.ok_or(IndexEventsError::MissingData)?;
+        let tx_hash = log
+            .transaction_hash
+            .ok_or(MaybeRecoverableError::MissingData)?
+            .0;
+        let block_number = log.block_number.ok_or(MaybeRecoverableError::MissingData)?;
 
         match log.topic0() {
             Some(&NewAccount::SIGNATURE_HASH) => {
@@ -117,8 +103,6 @@ fn process_logs(logs: Vec<Log>, connection: &Connection) -> Result<(), IndexEven
         };
     }
 
-    info!("Done");
-
     Ok(())
 }
 
diff --git a/crates/ar-cli/src/main.rs b/crates/ar-cli/src/main.rs
index 0a93035a..96375c36 100644
--- a/crates/ar-cli/src/main.rs
+++ b/crates/ar-cli/src/main.rs
@@ -1,6 +1,10 @@
+use std::{cmp::min, thread::sleep, time::Duration};
+
+use alloy_transport::TransportErrorKind;
 use clap::Parser;
 use cli::{ChainConfig, Cli};
-use log::info;
+use log::{error, info};
+use recoverable_error::MaybeRecoverableError;
 use thiserror::Error;
 
 mod cli;
@@ -8,9 +12,13 @@ mod collect_viewing_keys;
 mod db;
 mod generate;
 mod index_events;
+mod recoverable_error;
 mod reveal;
 mod revoke;
 
+const DEFAULT_BACKOFF: Duration = Duration::from_millis(2000); // 2 seconds
+const MAX_BACKOFF: Duration = Duration::from_millis(600000); // 10 minutes
+
 #[derive(Debug, Error)]
 #[error(transparent)]
 #[non_exhaustive]
@@ -18,11 +26,8 @@ enum CliError {
     #[error("Error generating keys")]
     Generate(#[from] generate::GenerateError),
 
-    #[error("Error collecting viewing keys")]
-    CollectKeys(#[from] collect_viewing_keys::CollectKeysError),
-
-    #[error("Error indexing events")]
-    Index(#[from] index_events::IndexEventsError),
+    #[error("Cannot recover from this variant of MaybeRecoverable")]
+    MaybeRecoverable(#[from] MaybeRecoverableError),
 
     #[error("Error revoking txs")]
     Revoke(#[from] revoke::RevokeError),
@@ -34,6 +39,30 @@ enum CliError {
     Db(#[from] rusqlite::Error),
 }
 
+pub async fn retry_with_backoff<F, Fut, T>(mut op: F) -> Result<T, MaybeRecoverableError>
+where
+    F: FnMut() -> Fut,
+    Fut: std::future::Future<Output = Result<T, MaybeRecoverableError>>,
+{
+    let mut delay = DEFAULT_BACKOFF;
+
+    loop {
+        match op().await {
+            Ok(result) => return Ok(result),
+
+            Err(MaybeRecoverableError::Rpc(alloy_json_rpc::RpcError::Transport(
+                TransportErrorKind::HttpError(http_err),
+            ))) if http_err.is_rate_limit_err() => {
+                delay = min(MAX_BACKOFF, delay * 2);
+                info!("Rate limited. Waiting {:?} before retrying.", delay);
+                sleep(delay);
+            }
+
+            Err(e) => return Err(e),
+        }
+    }
+}
+
 #[tokio::main(flavor = "current_thread")]
 async fn main() -> Result<(), CliError> {
     let config = Cli::parse();
@@ -59,18 +88,20 @@ async fn main() -> Result<(), CliError> {
                 },
             db,
         } => {
-            let connection = db::init(&db.path)?;
-            collect_viewing_keys::run(
-                rpc_url,
-                shielder_address,
-                private_key_file,
-                endianess.clone(),
-                *from_block,
-                connection,
-                *redact_sensitive_data,
-            )
+            retry_with_backoff(|| {
+                collect_viewing_keys::run(
+                    rpc_url,
+                    shielder_address,
+                    private_key_file,
+                    endianess.clone(),
+                    *from_block,
+                    &db.path,
+                    *redact_sensitive_data,
+                )
+            })
             .await?
         }
+
         cli::Command::IndexEvents {
             common:
                 ChainConfig {
@@ -81,20 +112,23 @@ async fn main() -> Result<(), CliError> {
             db,
             batch_size,
         } => {
-            let connection = db::init(&db.path)?;
-            index_events::run(
-                rpc_url,
-                shielder_address,
-                *from_block,
-                *batch_size,
-                connection,
-            )
+            retry_with_backoff(|| {
+                index_events::run(
+                    rpc_url,
+                    shielder_address,
+                    *from_block,
+                    *batch_size,
+                    &db.path,
+                )
+            })
             .await?
         }
+
         cli::Command::Revoke { db } => {
             let connection = db::init(&db.path)?;
             revoke::run(connection).await?
         }
+
         cli::Command::Reveal { db, tx_hash } => {
             let connection = db::init(&db.path)?;
             reveal::run(connection, tx_hash).await?
diff --git a/crates/ar-cli/src/recoverable_error.rs b/crates/ar-cli/src/recoverable_error.rs
new file mode 100644
index 00000000..881c7443
--- /dev/null
+++ b/crates/ar-cli/src/recoverable_error.rs
@@ -0,0 +1,34 @@
+use alloy_json_rpc::RpcError;
+use alloy_transport::TransportErrorKind;
+use hex::FromHexError;
+use shielder_contract::ShielderContractError;
+use thiserror::Error;
+
+#[derive(Debug, Error)]
+#[error(transparent)]
+#[non_exhaustive]
+pub enum MaybeRecoverableError {
+    #[error("Error while interacting with the Shielder contract")]
+    Contract(#[from] ShielderContractError),
+
+    #[error("RPC error")]
+    Rpc(#[from] RpcError<TransportErrorKind>),
+
+    #[error("Hex decoding error")]
+    HexError(#[from] FromHexError),
+
+    #[error("Error reading AR private key file")]
+    ARKeyRead(#[from] std::io::Error),
+
+    #[error("Error converting from a little-endian byte representation to grumpkin::Fr")]
+    NotAGrumpkinBaseFieldElement,
+
+    #[error("Error while persisting data")]
+    Db(#[from] rusqlite::Error),
+
+    #[error("Error while decoding event log")]
+    EventLog(#[from] alloy_sol_types::Error),
+
+    #[error("Event is missing some data")]
+    MissingData,
+}


Reviews:
user: login: pmikolajczyk41
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T09:16:27Z
user: login: fbielejec
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T19:30:58Z

Review comments:
diff_hunk: @@ -59,17 +67,44 @@ async fn main() -> Result<(), CliError> {
                 },
             db,
         } => {
-            let connection = db::init(&db.path)?;
-            collect_viewing_keys::run(
-                rpc_url,
-                shielder_address,
-                private_key_file,
-                endianess.clone(),
-                *from_block,
-                connection,
-                *redact_sensitive_data,
-            )
-            .await?
+            let mut delay = DEFAULT_BACKOFF;
+
+            loop {
path: crates/ar-cli/src/main.rs
user: login: pmikolajczyk41
body: I suggest introducing auxiliary function:
```rust
pub async fn retry_with_backoff<F, Fut, T>(mut op: F) -> Result<T, CollectKeysError>
where
    F: FnMut() -> Fut,
    Fut: std::future::Future<Output = Result<T, CollectKeysError>>,
{
    let mut delay = DEFAULT_BACKOFF;

    loop {
        match op().await {
            Ok(result) => return Ok(result),

            Err(CollectKeysError::Rpc(alloy_json_rpc::RpcError::Transport(
                TransportErrorKind::HttpError(http_err),
            ))) if http_err.is_rate_limit_err() => {
                delay = min(MAX_BACKOFF, delay * 2);
                info!("Rate limited. Waiting {:?} before retrying.", delay);
                sleep(delay);
            }

            Err(e) => return Err(e),
        }
    }
}
```

and calling it like:
```rust
retry_with_backoff(|| {
                collect_viewing_keys::run(
                    rpc_url,
                    shielder_address,
                    private_key_file,
                    endianess.clone(),
                    *from_block,
                    &db.path,
                    *redact_sensitive_data,
                )
            })
            .await?
```

here and below
created_at: 2025-03-27T09:15:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -59,17 +67,44 @@ async fn main() -> Result<(), CliError> {
                 },
             db,
         } => {
-            let connection = db::init(&db.path)?;
-            collect_viewing_keys::run(
-                rpc_url,
-                shielder_address,
-                private_key_file,
-                endianess.clone(),
-                *from_block,
-                connection,
-                *redact_sensitive_data,
-            )
-            .await?
+            let mut delay = DEFAULT_BACKOFF;
+
+            loop {
path: crates/ar-cli/src/main.rs
user: login: fbielejec
body: nice idea, done
created_at: 2025-03-27T19:30:58Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-26T22:55:11Z
body: <table><tr><th>Transaction Name</th><th>Main</th><th>Current</th><th>Difference (%)</th></tr><tr><td>Deposit (size)</td><td>20255</td><td>20255</td><td>0.00000%</td></tr><tr><td>DepositERC20 (gas)</td><td>1758338</td><td>1758338</td><td>0.00000%</td></tr><tr><td>DepositNative (gas)</td><td>1740261</td><td>1740225</td><td>-0.00207%</td></tr><tr><td>NewAccount (size)</td><td>22867</td><td>22867</td><td>0.00000%</td></tr><tr><td>NewAccountERC20 (gas)</td><td>1792041</td><td>1792161</td><td>+0.00670%</td></tr><tr><td>NewAccountNative (gas)</td><td>1996176</td><td>1996188</td><td>+0.00060%</td></tr><tr><td>Withdraw (size)</td><td>21574</td><td>21574</td><td>0.00000%</td></tr><tr><td>WithdrawERC20 (gas)</td><td>1860749</td><td>1860869</td><td>+0.00645%</td></tr><tr><td>WithdrawNative (gas)</td><td>1860606</td><td>1860642</td><td>+0.00193%</td></tr></table>
<!-- thollander/actions-comment-pull-request "hashFiles('report.html')" -->
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-26T22:55:23Z
body: # 📊 Coverage Report

## 📈 Total Coverage Summary

| Type | Covered | Total | Coverage |
|------|---------|--------|----------|
| 📝 Lines | 1479 | 2101 | 🟠 70.39% |
| 📄 Statements | 1479 | 2101 | 🟠 70.39% |
| ⚡ Functions | 107 | 114 | 🟡 93.85% |
| 🔀 Branches | 264 | 274 | 🟡 96.35% |

### Coverage Legend

- ✅ 100% Coverage
- 🟡 80-99% Coverage
- 🟠 50-79% Coverage
- ❌ 0-49% Coverage

## 📁 File Coverage

<details>
<summary>📋 Detailed Coverage Report</summary>

| File | Lines | Statements | Functions | Branches |
|------|-------|------------|-----------|----------|
| 🟡 [zkOS-monorepo/ts/shielder-sdk/src/actions/deposit.ts](zkOS-monorepo/ts/shielder-sdk/src/actions/deposit.ts) | 88.37% | 88.37% | 100% | 85% |
| 🟡 [zkOS-monorepo/ts/shielder-sdk/src/actions/newAccount.ts](zkOS-monorepo/ts/shielder-sdk/src/actions/newAccount.ts) | 84.37% | 84.37% | 100% | 78.94% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/actions/types.ts](zkOS-monorepo/ts/shielder-sdk/src/actions/types.ts) | 100% | 100% | 100% | 100% |
| 🟡 [zkOS-monorepo/ts/shielder-sdk/src/actions/utils.ts](zkOS-monorepo/ts/shielder-sdk/src/actions/utils.ts) | 97.36% | 97.36% | 100% | 90.9% |
| 🟠 [zkOS-monorepo/ts/shielder-sdk/src/actions/withdraw.ts](zkOS-monorepo/ts/shielder-sdk/src/actions/withdraw.ts) | 77.14% | 77.14% | 87.5% | 100% |
| ❌ [zkOS-monorepo/ts/shielder-sdk/src/chain/contract.ts](zkOS-monorepo/ts/shielder-sdk/src/chain/contract.ts) | 1.89% | 1.89% | 0% | 0% |
| ❌ [zkOS-monorepo/ts/shielder-sdk/src/chain/relayer.ts](zkOS-monorepo/ts/shielder-sdk/src/chain/relayer.ts) | 13.49% | 13.49% | 0% | 0% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/client/actions.ts](zkOS-monorepo/ts/shielder-sdk/src/client/actions.ts) | 100% | 100% | 100% | 100% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/client/client.ts](zkOS-monorepo/ts/shielder-sdk/src/client/client.ts) | 100% | 100% | 100% | 100% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/client/factories.ts](zkOS-monorepo/ts/shielder-sdk/src/client/factories.ts) | 100% | 100% | 100% | 100% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/client/types.ts](zkOS-monorepo/ts/shielder-sdk/src/client/types.ts) | 100% | 100% | 100% | 100% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/constants.ts](zkOS-monorepo/ts/shielder-sdk/src/constants.ts) | 100% | 100% | 100% | 100% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/errors.ts](zkOS-monorepo/ts/shielder-sdk/src/errors.ts) | 100% | 100% | 100% | 100% |
| ❌ [zkOS-monorepo/ts/shielder-sdk/src/index.ts](zkOS-monorepo/ts/shielder-sdk/src/index.ts) | 0% | 0% | 100% | 100% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/state/accountFactory.ts](zkOS-monorepo/ts/shielder-sdk/src/state/accountFactory.ts) | 100% | 100% | 100% | 100% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/state/accountRegistry.ts](zkOS-monorepo/ts/shielder-sdk/src/state/accountRegistry.ts) | 100% | 100% | 100% | 100% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/state/accountStateSerde.ts](zkOS-monorepo/ts/shielder-sdk/src/state/accountStateSerde.ts) | 100% | 100% | 100% | 100% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/state/idManager.ts](zkOS-monorepo/ts/shielder-sdk/src/state/idManager.ts) | 100% | 100% | 100% | 100% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/state/localStateTransition.ts](zkOS-monorepo/ts/shielder-sdk/src/state/localStateTransition.ts) | 100% | 100% | 100% | 100% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/state/sync/chainStateTransition.ts](zkOS-monorepo/ts/shielder-sdk/src/state/sync/chainStateTransition.ts) | 100% | 100% | 100% | 100% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/state/sync/historyFetcher.ts](zkOS-monorepo/ts/shielder-sdk/src/state/sync/historyFetcher.ts) | 100% | 100% | 100% | 100% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/state/sync/synchronizer.ts](zkOS-monorepo/ts/shielder-sdk/src/state/sync/synchronizer.ts) | 100% | 100% | 100% | 100% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/state/sync/tokenAccountFinder.ts](zkOS-monorepo/ts/shielder-sdk/src/state/sync/tokenAccountFinder.ts) | 100% | 100% | 100% | 100% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/state/types.ts](zkOS-monorepo/ts/shielder-sdk/src/state/types.ts) | 100% | 100% | 100% | 100% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/storage/storageManager.ts](zkOS-monorepo/ts/shielder-sdk/src/storage/storageManager.ts) | 100% | 100% | 100% | 100% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/storage/storageSchema.ts](zkOS-monorepo/ts/shielder-sdk/src/storage/storageSchema.ts) | 100% | 100% | 100% | 100% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/types.ts](zkOS-monorepo/ts/shielder-sdk/src/types.ts) | 100% | 100% | 100% | 100% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/utils.ts](zkOS-monorepo/ts/shielder-sdk/src/utils.ts) | 100% | 100% | 100% | 100% |

</details>

<!-- Sticky Pull Request Comment -->
author_association: NONE