Эх сурвалжийг харах

drk: introduce scanned blocks table to store a blocks rollback query

skoupidi 1 жил өмнө
parent
commit
ca22db1d1e

+ 200 - 82
bin/drk/src/dao.rs

@@ -429,6 +429,30 @@ impl Drk {
         Ok((daos_tree, proposals_tree))
     }
 
+    /// Auxiliary function to fetch the current DAO Merkle trees state,
+    /// as an update query.
+    pub async fn get_dao_trees_state_query(&self) -> Result<String> {
+        // Grab current DAO trees
+        let (daos_tree, proposals_tree) = self.get_dao_trees().await?;
+
+        // Create the update query
+        match self.wallet.create_prepared_statement(
+            &format!(
+                "UPDATE {} SET {} = ?1, {} = ?2;",
+                *DAO_TREES_TABLE, DAO_TREES_COL_DAOS_TREE, DAO_TREES_COL_PROPOSALS_TREE
+            ),
+            rusqlite::params![
+                serialize_async(&daos_tree).await,
+                serialize_async(&proposals_tree).await
+            ],
+        ) {
+            Ok(q) => Ok(q),
+            Err(e) => Err(Error::DatabaseError(format!(
+                "[get_dao_trees_state_query] Creating query for DAO trees failed: {e:?}"
+            ))),
+        }
+    }
+
     /// Fetch all DAO secret keys from the wallet.
     pub async fn get_dao_secrets(&self) -> Result<Vec<SecretKey>> {
         let daos = self.get_daos().await?;
@@ -633,7 +657,8 @@ impl Drk {
         Ok(proposals)
     }
 
-    /// Auxiliary function to apply `DaoFunction::Mint` call data to the wallet.
+    /// Auxiliary function to apply `DaoFunction::Mint` call data to the wallet,
+    /// and store its inverse query into the cache.
     /// Returns a flag indicating if the provided call refers to our own wallet.
     async fn apply_dao_mint_data(
         &self,
@@ -676,7 +701,8 @@ impl Drk {
         Ok(false)
     }
 
-    /// Auxiliary function to apply `DaoFunction::Propose` call data to the wallet.
+    /// Auxiliary function to apply `DaoFunction::Propose` call data to the wallet,
+    /// and store its inverse query into the cache.
     /// Returns a flag indicating if the provided call refers to our own wallet.
     async fn apply_dao_propose_data(
         &self,
@@ -741,7 +767,8 @@ impl Drk {
         Ok(false)
     }
 
-    /// Auxiliary function to apply `DaoFunction::Vote` call data to the wallet.
+    /// Auxiliary function to apply `DaoFunction::Vote` call data to the wallet,
+    /// and store its inverse query into the cache.
     /// Returns a flag indicating if the provided call refers to our own wallet.
     async fn apply_dao_vote_data(
         &self,
@@ -812,7 +839,8 @@ impl Drk {
         Ok(true)
     }
 
-    /// Auxiliary function to apply `DaoFunction::Exec` call data to the wallet.
+    /// Auxiliary function to apply `DaoFunction::Exec` call data to the wallet,
+    /// and store its inverse query into the cache.
     /// Returns a flag indicating if the provided call refers to our own wallet.
     async fn apply_dao_exec_data(
         &self,
@@ -820,22 +848,57 @@ impl Drk {
         tx_hash: TransactionHash,
     ) -> Result<bool> {
         // Check if we got the corresponding proposal
-        let Ok(mut proposal) = self.get_dao_proposal_by_bulla(&params.proposal_bulla).await else {
+        if self.get_dao_proposal_by_bulla(&params.proposal_bulla).await.is_err() {
             return Ok(false)
         };
 
-        // Update its exec transaction hash
-        proposal.exec_tx_hash = Some(tx_hash);
-        if let Err(e) = self.put_dao_proposal(&proposal).await {
+        // Grab proposal record key
+        let key = serialize_async(&params.proposal_bulla).await;
+
+        // Create an SQL `UPDATE` query to update proposal exec transaction hash
+        let query = format!(
+            "UPDATE {} SET {} = ?1 WHERE {} = ?2;",
+            *DAO_PROPOSALS_TABLE, DAO_PROPOSALS_COL_EXEC_TX_HASH, DAO_PROPOSALS_COL_BULLA,
+        );
+
+        // Create its inverse query
+        let inverse = match self.wallet.create_prepared_statement(
+            &format!(
+                "UPDATE {} SET {} = NULL WHERE {} = ?1;",
+                *DAO_PROPOSALS_TABLE, DAO_PROPOSALS_COL_EXEC_TX_HASH, DAO_PROPOSALS_COL_BULLA,
+            ),
+            rusqlite::params![key],
+        ) {
+            Ok(q) => q,
+            Err(e) => {
+                return Err(Error::DatabaseError(format!(
+                    "[apply_dao_exec_data] Creating DAO proposal update inverse query failed: {e:?}"
+                )))
+            }
+        };
+
+        // Execute the query
+        if let Err(e) = self
+            .wallet
+            .exec_sql(&query, rusqlite::params![Some(serialize_async(&tx_hash).await), key])
+        {
+            return Err(Error::DatabaseError(format!(
+                "[apply_dao_exec_data] Update DAO proposal failed: {e:?}"
+            )))
+        }
+
+        // Store its inverse
+        if let Err(e) = self.wallet.cache_inverse(inverse) {
             return Err(Error::DatabaseError(format!(
-                "[apply_dao_exec_data] Put DAO proposal failed: {e:?}"
+                "[apply_dao_exec_data] Inserting inverse query into cache failed: {e:?}"
             )))
         }
 
         Ok(true)
     }
 
-    /// Append data related to DAO contract transactions into the wallet database.
+    /// Append data related to DAO contract transactions into the wallet database,
+    /// and store their inverse queries into the cache.
     /// Returns a flag indicating if the provided data refer to our own wallet.
     pub async fn apply_tx_dao_data(
         &self,
@@ -873,10 +936,15 @@ impl Drk {
         }
     }
 
-    /// Confirm already imported DAO metadata into the wallet.
+    /// Confirm already imported DAO metadata into the wallet,
+    /// and store its inverse query into the cache.
     /// Here we just write the leaf position, tx hash, and call index.
     /// Panics if the fields are None.
     pub async fn confirm_dao(&self, dao: &DaoRecord) -> WalletDbResult<()> {
+        // Grab dao record key
+        let key = serialize_async(&dao.bulla()).await;
+
+        // Create an SQL `UPDATE` query
         let query = format!(
             "UPDATE {} SET {} = ?1, {} = ?2, {} = ?3 WHERE {} = ?4;",
             *DAO_DAOS_TABLE,
@@ -885,44 +953,38 @@ impl Drk {
             DAO_DAOS_COL_CALL_INDEX,
             DAO_DAOS_COL_BULLA
         );
-        self.wallet.exec_sql(
-            &query,
-            rusqlite::params![
-                serialize_async(&dao.leaf_position.unwrap()).await,
-                serialize_async(&dao.tx_hash.unwrap()).await,
-                dao.call_index.unwrap(),
-                serialize_async(&dao.bulla()).await,
-            ],
-        )
-    }
 
-    /// Unconfirm imported DAOs by removing the leaf position, tx hash, and call index.
-    pub async fn unconfirm_daos(&self, daos: &[DaoRecord]) -> WalletDbResult<()> {
-        for dao in daos {
-            let query = format!(
-                "UPDATE {} SET {} = ?1, {} = ?2, {} = ?3 WHERE {} = ?4;",
-                *DAO_DAOS_TABLE,
-                DAO_DAOS_COL_LEAF_POSITION,
-                DAO_DAOS_COL_TX_HASH,
-                DAO_DAOS_COL_CALL_INDEX,
-                DAO_DAOS_COL_BULLA
-            );
-            self.wallet.exec_sql(
-                &query,
-                rusqlite::params![
-                    None::<Vec<u8>>,
-                    None::<Vec<u8>>,
-                    None::<u64>,
-                    serialize_async(&dao.bulla()).await
-                ],
-            )?;
-        }
+        // Create its params
+        let params = rusqlite::params![
+            serialize_async(&dao.leaf_position.unwrap()).await,
+            serialize_async(&dao.tx_hash.unwrap()).await,
+            dao.call_index.unwrap(),
+            key,
+        ];
 
-        Ok(())
+        // Create its inverse query
+        let inverse_query = format!(
+            "UPDATE {} SET {} = NULL, {} = NULL, {} = NULL WHERE {} = ?1;",
+            *DAO_DAOS_TABLE,
+            DAO_DAOS_COL_LEAF_POSITION,
+            DAO_DAOS_COL_TX_HASH,
+            DAO_DAOS_COL_CALL_INDEX,
+            DAO_DAOS_COL_BULLA
+        );
+        let inverse =
+            self.wallet.create_prepared_statement(&inverse_query, rusqlite::params![key])?;
+
+        // Execute the query
+        self.wallet.exec_sql(&query, params)?;
+
+        // Store its inverse
+        self.wallet.cache_inverse(inverse)
     }
 
-    /// Import given DAO proposal into the wallet.
+    /// Import given DAO proposal into the wallet,
+    /// and store its inverse query into the cache.
     pub async fn put_dao_proposal(&self, proposal: &ProposalRecord) -> Result<()> {
+        // Check that we already have the proposal DAO
         if let Err(e) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await {
             return Err(Error::DatabaseError(format!(
                 "[put_dao_proposal] Couldn't find proposal {} DAO {}: {e}",
@@ -931,6 +993,10 @@ impl Drk {
             )))
         }
 
+        // Grab proposal record key
+        let key = serialize_async(&proposal.bulla()).await;
+
+        // Create an SQL `INSERT OR REPLACE` query
         let query = format!(
             "INSERT OR REPLACE INTO {} ({}, {}, {}, {}, {}, {}, {}, {}, {}, {}) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10);",
             *DAO_PROPOSALS_TABLE,
@@ -946,6 +1012,7 @@ impl Drk {
             DAO_PROPOSALS_COL_EXEC_TX_HASH,
         );
 
+        // Create its params
         let data = match &proposal.data {
             Some(data) => Some(data),
             None => None,
@@ -976,26 +1043,54 @@ impl Drk {
             None => None,
         };
 
-        if let Err(e) = self.wallet.exec_sql(
-            &query,
-            rusqlite::params![
-                serialize_async(&proposal.bulla()).await,
-                serialize_async(&proposal.proposal.dao_bulla).await,
-                serialize_async(&proposal.proposal).await,
-                data,
-                leaf_position,
-                money_snapshot_tree,
-                nullifiers_smt_snapshot,
-                tx_hash,
-                proposal.call_index,
-                exec_tx_hash,
-            ],
-        ) {
+        let params = rusqlite::params![
+            key,
+            serialize_async(&proposal.proposal.dao_bulla).await,
+            serialize_async(&proposal.proposal).await,
+            data,
+            leaf_position,
+            money_snapshot_tree,
+            nullifiers_smt_snapshot,
+            tx_hash,
+            proposal.call_index,
+            exec_tx_hash,
+        ];
+
+        // Create its inverse query
+        let inverse_query = format!(
+            "UPDATE {} SET {} = NULL, {} = NULL, {} = NULL, {} = NULL, {} = NULL, WHERE {} = ?1;",
+            *DAO_PROPOSALS_TABLE,
+            DAO_PROPOSALS_COL_LEAF_POSITION,
+            DAO_PROPOSALS_COL_MONEY_SNAPSHOT_TREE,
+            DAO_PROPOSALS_COL_NULLIFIERS_SMT_SNAPSHOT,
+            DAO_PROPOSALS_COL_TX_HASH,
+            DAO_PROPOSALS_COL_CALL_INDEX,
+            DAO_PROPOSALS_COL_BULLA,
+        );
+        let inverse =
+            match self.wallet.create_prepared_statement(&inverse_query, rusqlite::params![key]) {
+                Ok(q) => q,
+                Err(e) => {
+                    return Err(Error::DatabaseError(format!(
+                    "[put_dao_proposal] Creating DAO proposal insert inverse query failed: {e:?}"
+                )))
+                }
+            };
+
+        // Execute the query
+        if let Err(e) = self.wallet.exec_sql(&query, params) {
             return Err(Error::DatabaseError(format!(
                 "[put_dao_proposal] Proposal insert failed: {e:?}"
             )))
         };
 
+        // Store its inverse
+        if let Err(e) = self.wallet.cache_inverse(inverse) {
+            return Err(Error::DatabaseError(format!(
+                "[put_dao_proposal] Inserting inverse query into cache failed: {e:?}"
+            )))
+        }
+
         Ok(())
     }
 
@@ -1030,10 +1125,12 @@ impl Drk {
         Ok(())
     }
 
-    /// Import given DAO votes into the wallet.
+    /// Import given DAO vote into the wallet,
+    /// and store its inverse query into the cache.
     pub async fn put_dao_vote(&self, vote: &VoteRecord) -> WalletDbResult<()> {
-        eprintln!("Importing DAO vote into wallet");
+        println!("Importing DAO vote into wallet");
 
+        // Create an SQL `INSERT OR REPLACE` query
         let query = format!(
             "INSERT INTO {} ({}, {}, {}, {}, {}, {}, {}, {}) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8);",
             *DAO_VOTES_TABLE,
@@ -1047,19 +1144,40 @@ impl Drk {
             DAO_VOTES_COL_NULLIFIERS,
         );
 
-        self.wallet.exec_sql(
-            &query,
-            rusqlite::params![
-                serialize_async(&vote.proposal).await,
-                vote.vote_option as u64,
-                serialize_async(&vote.yes_vote_blind).await,
-                serialize_async(&vote.all_vote_value).await,
-                serialize_async(&vote.all_vote_blind).await,
-                serialize_async(&vote.tx_hash).await,
-                vote.call_index,
-                serialize_async(&vote.nullifiers).await,
-            ],
-        )?;
+        // Create its params
+        let params = rusqlite::params![
+            serialize_async(&vote.proposal).await,
+            vote.vote_option as u64,
+            serialize_async(&vote.yes_vote_blind).await,
+            serialize_async(&vote.all_vote_value).await,
+            serialize_async(&vote.all_vote_blind).await,
+            serialize_async(&vote.tx_hash).await,
+            vote.call_index,
+            serialize_async(&vote.nullifiers).await,
+        ];
+
+        // Create its inverse query.
+        // Since we don't know the record ID we will remove it
+        // using all its fields.
+        let inverse_query = format!(
+            "DELETE FROM {} WHERE {} = ?1, {} = ?2, {} = ?3, {} = ?4, {} = ?5, {} = ?6, {} = ?7, {} = ?8;",
+            *DAO_VOTES_TABLE,
+            DAO_VOTES_COL_PROPOSAL_BULLA,
+            DAO_VOTES_COL_VOTE_OPTION,
+            DAO_VOTES_COL_YES_VOTE_BLIND,
+            DAO_VOTES_COL_ALL_VOTE_VALUE,
+            DAO_VOTES_COL_ALL_VOTE_BLIND,
+            DAO_VOTES_COL_TX_HASH,
+            DAO_VOTES_COL_CALL_INDEX,
+            DAO_VOTES_COL_NULLIFIERS,
+        );
+        let inverse = self.wallet.create_prepared_statement(&inverse_query, params)?;
+
+        // Execute the query
+        self.wallet.exec_sql(&query, params)?;
+
+        // Store its inverse
+        self.wallet.cache_inverse(inverse)?;
 
         println!("DAO vote added to wallet");
 
@@ -1079,14 +1197,14 @@ impl Drk {
     /// Reset confirmed DAOs in the wallet.
     pub async fn reset_daos(&self) -> WalletDbResult<()> {
         println!("Resetting DAO confirmations");
-        let daos = match self.get_daos().await {
-            Ok(d) => d,
-            Err(e) => {
-                println!("[reset_daos] DAOs retrieval failed: {e:?}");
-                return Err(WalletDbError::GenericError);
-            }
-        };
-        self.unconfirm_daos(&daos).await?;
+        let query = format!(
+            "UPDATE {} SET {} = NULL, {} = NULL, {} = NULL;",
+            *DAO_DAOS_TABLE,
+            DAO_DAOS_COL_LEAF_POSITION,
+            DAO_DAOS_COL_TX_HASH,
+            DAO_DAOS_COL_CALL_INDEX,
+        );
+        self.wallet.exec_sql(&query, &[])?;
         println!("Successfully unconfirmed DAOs");
 
         Ok(())

+ 66 - 0
bin/drk/src/lib.rs

@@ -54,6 +54,9 @@ pub mod deploy;
 /// Wallet functionality related to transactions history
 pub mod txs_history;
 
+/// Wallet functionality related to scanned blocks
+pub mod scanned_blocks;
+
 /// Wallet database operations handler
 pub mod walletdb;
 use walletdb::{WalletDb, WalletPtr};
@@ -151,4 +154,67 @@ impl Drk {
 
         Ok((height, hash.clone()))
     }
+
+    /// Auxiliary function to reset `walletdb` inverse cache state.
+    /// Additionally, set current trees state inverse queries.
+    /// We keep the entire trees state as two distinct inverse queries,
+    /// since we execute per transaction call, so we don't have to update
+    /// them on each iteration.
+    pub async fn reset_inverse_cache(&self) -> Result<()> {
+        // Reset `walletdb` inverse cache
+        if let Err(e) = self.wallet.clear_inverse_cache() {
+            return Err(Error::DatabaseError(format!(
+                "[reset_inverse_cache] Clearing wallet inverse cache failed: {e:?}"
+            )))
+        }
+
+        // Grab current money tree state query and insert it into inverse cache
+        let query = self.get_money_tree_state_query().await?;
+        if let Err(e) = self.wallet.cache_inverse(query) {
+            return Err(Error::DatabaseError(format!(
+                "[reset_inverse_cache] Inserting money query into inverse cache failed: {e:?}"
+            )))
+        }
+
+        // Grab current DAO trees state query and insert it into inverse cache
+        let query = self.get_dao_trees_state_query().await?;
+        if let Err(e) = self.wallet.cache_inverse(query) {
+            return Err(Error::DatabaseError(format!(
+                "[reset_inverse_cache] Inserting DAO query into inverse cache failed: {e:?}"
+            )))
+        }
+
+        Ok(())
+    }
+
+    /// Auxiliary function to store current `walletdb` inverse cache
+    /// in scanned blocks information for provided block height and hash.
+    /// Additionally, clear `walletdb` inverse cache state.
+    pub fn store_inverse_cache(&self, height: u32, hash: &str) -> Result<()> {
+        // Grab current inverse state rollback query
+        let rollback_query = match self.wallet.grab_inverse_cache_block() {
+            Ok(q) => q,
+            Err(e) => {
+                return Err(Error::DatabaseError(format!(
+                    "[store_inverse_cache] Creating rollback query failed: {e:?}"
+                )))
+            }
+        };
+
+        // Store it as a scanned blocks information record
+        if let Err(e) = self.put_scanned_block_record(height, hash, &rollback_query) {
+            return Err(Error::DatabaseError(format!(
+                "[store_inverse_cache] Inserting scanned blocks information record failed: {e:?}"
+            )))
+        };
+
+        // Reset `walletdb` inverse cache
+        if let Err(e) = self.wallet.clear_inverse_cache() {
+            return Err(Error::DatabaseError(format!(
+                "[store_inverse_cache] Clearing wallet inverse cache failed: {e:?}"
+            )))
+        };
+
+        Ok(())
+    }
 }

+ 110 - 23
bin/drk/src/money.rs

@@ -665,6 +665,24 @@ impl Drk {
         Ok(tree)
     }
 
+    /// Auxiliary function to fetch the current Money Merkle tree state,
+    /// as an update query.
+    pub async fn get_money_tree_state_query(&self) -> Result<String> {
+        // Grab current money tree
+        let tree = self.get_money_tree().await?;
+
+        // Create the update query
+        match self.wallet.create_prepared_statement(
+            &format!("UPDATE {} SET {} = ?1;", *MONEY_TREE_TABLE, MONEY_TREE_COL_TREE),
+            rusqlite::params![serialize_async(&tree).await],
+        ) {
+            Ok(q) => Ok(q),
+            Err(e) => Err(Error::DatabaseError(format!(
+                "[get_money_tree_state_query] Creating query for money tree failed: {e:?}"
+            ))),
+        }
+    }
+
     /// Fetch the Money nullifiers SMT from the wallet, as a map.
     pub async fn get_nullifiers_smt(&self) -> Result<HashMap<BigUint, pallas::Base>> {
         let rows = match self.wallet.query_multiple(&MONEY_SMT_TABLE, &[], &[]) {
@@ -783,7 +801,8 @@ impl Drk {
         Ok((nullifiers, coins, notes, freezes))
     }
 
-    /// Append data related to Money contract transactions into the wallet database.
+    /// Append data related to Money contract transactions into the wallet database,
+    /// and store their inverse queries into the cache.
     /// Returns a flag indicating if the provided data refer to our own wallet.
     pub async fn apply_tx_money_data(
         &self,
@@ -825,8 +844,7 @@ impl Drk {
         self.smt_insert(&nullifiers)?;
         let wallet_spent_coins = self.mark_spent_coins(&nullifiers, tx_hash).await?;
 
-        // This is the SQL query we'll be executing to insert new coins
-        // into the wallet
+        // This is the SQL query we'll be executing to insert new coins into the wallet
         let query = format!(
             "INSERT INTO {} ({}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12);",
             *MONEY_COINS_TABLE,
@@ -844,11 +862,31 @@ impl Drk {
             MONEY_COINS_COL_MEMO,
         );
 
+        // This is its inverse query
+        let inverse_query =
+            format!("DELETE FROM {} WHERE {} = ?1;", *MONEY_COINS_TABLE, MONEY_COINS_COL_COIN);
+
         println!("Found {} OwnCoin(s) in transaction", owncoins.len());
         for owncoin in &owncoins {
             println!("OwnCoin: {:?}", owncoin.coin);
+            // Grab coin record key
+            let key = serialize_async(&owncoin.coin).await;
+
+            // Create its inverse query
+            let inverse =
+                match self.wallet.create_prepared_statement(&inverse_query, rusqlite::params![key])
+                {
+                    Ok(q) => q,
+                    Err(e) => {
+                        return Err(Error::DatabaseError(format!(
+                    "[apply_tx_money_data] Creating Money coin insert inverse query failed: {e:?}"
+                )))
+                    }
+                };
+
+            // Execute the query
             let params = rusqlite::params![
-                serialize_async(&owncoin.coin).await,
+                key,
                 0, // <-- is_spent
                 serialize_async(&owncoin.note.value).await,
                 serialize_async(&owncoin.note.token_id).await,
@@ -867,31 +905,63 @@ impl Drk {
                     "[apply_tx_money_data] Inserting Money coin failed: {e:?}"
                 )))
             }
+
+            // Store its inverse
+            if let Err(e) = self.wallet.cache_inverse(inverse) {
+                return Err(Error::DatabaseError(format!(
+                    "[apply_tx_money_data] Inserting inverse query into cache failed: {e:?}"
+                )))
+            }
         }
 
-        let mut wallet_freezes = false;
-        for token_id in freezes {
-            let query = format!(
-                "UPDATE {} SET {} = 1 WHERE {} = ?1;",
-                *MONEY_TOKENS_TABLE, MONEY_TOKENS_COL_IS_FROZEN, MONEY_TOKENS_COL_TOKEN_ID,
-            );
+        // This is the SQL query we'll be executing to update frozen tokens into the wallet
+        let query = format!(
+            "UPDATE {} SET {} = 1 WHERE {} = ?1;",
+            *MONEY_TOKENS_TABLE, MONEY_TOKENS_COL_IS_FROZEN, MONEY_TOKENS_COL_TOKEN_ID,
+        );
 
-            if let Err(e) =
-                self.wallet.exec_sql(&query, rusqlite::params![serialize_async(&token_id).await])
-            {
+        // This is its inverse query
+        let inverse_query = format!(
+            "UPDATE {} SET {} = 0 WHERE {} = ?1;",
+            *MONEY_TOKENS_TABLE, MONEY_TOKENS_COL_IS_FROZEN, MONEY_TOKENS_COL_TOKEN_ID,
+        );
+
+        for token_id in &freezes {
+            // Grab token record key
+            let key = serialize_async(token_id).await;
+
+            // Create its inverse query
+            let inverse =
+                match self.wallet.create_prepared_statement(&inverse_query, rusqlite::params![key])
+                {
+                    Ok(q) => q,
+                    Err(e) => {
+                        return Err(Error::DatabaseError(format!(
+                    "[apply_tx_money_data] Creating Money token freeze inverse query failed: {e:?}"
+                )))
+                    }
+                };
+
+            // Execute the query
+            if let Err(e) = self.wallet.exec_sql(&query, rusqlite::params![key]) {
                 return Err(Error::DatabaseError(format!(
-                    "[apply_tx_money_data] Inserting Money coin failed: {e:?}"
+                    "[apply_tx_money_data] Update Money token freeze failed: {e:?}"
                 )))
             }
 
-            wallet_freezes = true;
+            // Store its inverse
+            if let Err(e) = self.wallet.cache_inverse(inverse) {
+                return Err(Error::DatabaseError(format!(
+                    "[apply_tx_money_data] Inserting inverse query into cache failed: {e:?}"
+                )))
+            }
         }
 
         if self.fun && !owncoins.is_empty() {
             kaching().await;
         }
 
-        Ok(wallet_spent_coins || !owncoins.is_empty() || wallet_freezes)
+        Ok(wallet_spent_coins || !owncoins.is_empty() || !freezes.is_empty())
     }
 
     /// Auxiliary function to  grab all the nullifiers from a transaction money call.
@@ -941,20 +1011,37 @@ impl Drk {
         Ok(())
     }
 
-    /// Mark a coin in the wallet as spent.
+    /// Mark a coin in the wallet as spent, and store its inverse query into the cache.
     pub async fn mark_spent_coin(&self, coin: &Coin, spent_tx_hash: &String) -> WalletDbResult<()> {
+        // Grab coin record key
+        let key = serialize_async(&coin.inner()).await;
+
+        // Create an SQL `UPDATE` query to mark rows as spent(1)
         let query = format!(
-            "UPDATE {} SET {} = ?1, {} = ?2 WHERE {} = ?3;",
+            "UPDATE {} SET {} = 1, {} = ?1 WHERE {} = ?2;",
             *MONEY_COINS_TABLE,
             MONEY_COINS_COL_IS_SPENT,
             MONEY_COINS_COL_SPENT_TX_HASH,
             MONEY_COINS_COL_COIN
         );
-        let is_spent = 1;
-        self.wallet.exec_sql(
-            &query,
-            rusqlite::params![is_spent, spent_tx_hash, serialize_async(&coin.inner()).await],
-        )
+
+        // Create its inverse query
+        let inverse = self.wallet.create_prepared_statement(
+            &format!(
+                "UPDATE {} SET {} = 0, {} = '-' WHERE {} = ?1;",
+                *MONEY_COINS_TABLE,
+                MONEY_COINS_COL_IS_SPENT,
+                MONEY_COINS_COL_SPENT_TX_HASH,
+                MONEY_COINS_COL_COIN
+            ),
+            rusqlite::params![key],
+        )?;
+
+        // Execute the query
+        self.wallet.exec_sql(&query, rusqlite::params![spent_tx_hash, key])?;
+
+        // Store its inverse
+        self.wallet.cache_inverse(inverse)
     }
 
     /// Marks all coins in the wallet as spent, if their nullifier is in the given set.

+ 8 - 1
bin/drk/src/rpc.rs

@@ -182,8 +182,11 @@ impl Drk {
 
     /// `scan_block` will go over over transactions in a block and handle their calls
     /// based on the called contract. Additionally, will update `last_scanned_block` to
-    /// the probided block height.
+    /// the provided block height and will store its height, hash and inverse query.
     async fn scan_block(&self, block: &BlockInfo) -> Result<()> {
+        // Reset wallet inverse cache state
+        self.reset_inverse_cache().await?;
+
         // Keep track of our wallet transactions
         let mut wallet_txs = vec![];
         println!("=======================================");
@@ -243,6 +246,9 @@ impl Drk {
             )))
         }
 
+        // Store this block rollback query
+        self.store_inverse_cache(block.header.height, &block.hash().to_string())?;
+
         // Write this block height into `last_scanned_block`
         if let Err(e) =
             self.update_last_scanned_block(block.header.height, &block.hash().to_string())
@@ -266,6 +272,7 @@ impl Drk {
         // has been provided we reset, otherwise continue with
         // the next block height
         if height == 0 || reset {
+            self.reset_scanned_blocks()?;
             self.reset_money_tree().await?;
             self.reset_money_smt()?;
             self.reset_money_coins()?;

+ 94 - 0
bin/drk/src/scanned_blocks.rs

@@ -0,0 +1,94 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2024 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use rusqlite::types::Value;
+
+use darkfi::{Error, Result};
+
+use crate::{convert_named_params, error::WalletDbResult, Drk};
+
+// Wallet SQL table constant names. These have to represent the `wallet.sql`
+// SQL schema.
+const WALLET_SCANNED_BLOCKS_TABLE: &str = "scanned_blocks";
+const WALLET_SCANNED_BLOCKS_COL_HEIGH: &str = "height";
+const WALLET_SCANNED_BLOCKS_COL_HASH: &str = "hash";
+const WALLET_SCANNED_BLOCKS_COL_ROLLBACK_QUERY: &str = "rollback_query";
+
+impl Drk {
+    /// Insert a scanned block information record into the wallet.
+    pub fn put_scanned_block_record(
+        &self,
+        height: u32,
+        hash: &str,
+        rollback_query: &str,
+    ) -> WalletDbResult<()> {
+        let query = format!(
+            "INSERT INTO {} ({}, {}, {}) VALUES (?1, ?2, ?3);",
+            WALLET_SCANNED_BLOCKS_TABLE,
+            WALLET_SCANNED_BLOCKS_COL_HEIGH,
+            WALLET_SCANNED_BLOCKS_COL_HASH,
+            WALLET_SCANNED_BLOCKS_COL_ROLLBACK_QUERY,
+        );
+        self.wallet.exec_sql(&query, rusqlite::params![height, hash, rollback_query])
+    }
+
+    /// Get a scanned block information record.
+    pub fn get_scanned_block_record(&self, height: u32) -> Result<(u32, String, String)> {
+        let row = match self.wallet.query_single(
+            WALLET_SCANNED_BLOCKS_TABLE,
+            &[],
+            convert_named_params! {(WALLET_SCANNED_BLOCKS_COL_HEIGH, height)},
+        ) {
+            Ok(r) => r,
+            Err(e) => {
+                return Err(Error::DatabaseError(format!(
+                    "[get_scanned_block_record] Scanned block information record retrieval failed: {e:?}"
+                )))
+            }
+        };
+
+        let Value::Integer(height) = row[0] else {
+            return Err(Error::ParseFailed("[get_scanned_block_record] Block height parsing failed"))
+        };
+        let Ok(height) = u32::try_from(height) else {
+            return Err(Error::ParseFailed("[get_scanned_block_record] Block height parsing failed"))
+        };
+
+        let Value::Text(ref hash) = row[1] else {
+            return Err(Error::ParseFailed("[get_scanned_block_record] Hash parsing failed"))
+        };
+
+        let Value::Text(ref rollback_query) = row[2] else {
+            return Err(Error::ParseFailed(
+                "[get_scanned_block_record] Rollback query parsing failed",
+            ))
+        };
+
+        Ok((height, hash.clone(), rollback_query.clone()))
+    }
+
+    /// Reset the scanned blocks information records in the wallet.
+    pub fn reset_scanned_blocks(&self) -> WalletDbResult<()> {
+        println!("Resetting scanned blocks");
+        let query = format!("DELETE FROM {};", WALLET_SCANNED_BLOCKS_TABLE);
+        self.wallet.exec_sql(&query, &[])?;
+        println!("Successfully reset scanned blocks");
+
+        Ok(())
+    }
+}

+ 19 - 1
bin/drk/src/txs_history.rs

@@ -36,12 +36,13 @@ const WALLET_TXS_HISTORY_COL_TX: &str = "tx";
 
 impl Drk {
     /// Insert or update a `Transaction` history record into the wallet,
-    /// with the provided status.
+    /// with the provided status, and store its inverse query into the cache.
     pub async fn put_tx_history_record(
         &self,
         tx: &Transaction,
         status: &str,
     ) -> WalletDbResult<String> {
+        // Create an SQL `INSERT OR REPLACE` query
         let query = format!(
             "INSERT OR REPLACE INTO {} ({}, {}, {}) VALUES (?1, ?2, ?3);",
             WALLET_TXS_HISTORY_TABLE,
@@ -49,10 +50,27 @@ impl Drk {
             WALLET_TXS_HISTORY_COL_STATUS,
             WALLET_TXS_HISTORY_COL_TX,
         );
+
+        // Create its inverse query
         let tx_hash = tx.hash().to_string();
+        // We only need to reverse the transaction status to "Broadcasted"
+        let inverse = self.wallet.create_prepared_statement(
+            &format!(
+                "UPDATE {} SET {} = ?1 WHERE {} = ?2;",
+                WALLET_TXS_HISTORY_TABLE,
+                WALLET_TXS_HISTORY_COL_STATUS,
+                WALLET_TXS_HISTORY_COL_TX_HASH
+            ),
+            rusqlite::params!["Broadcasted", tx_hash],
+        )?;
+
+        // Execute the query
         self.wallet
             .exec_sql(&query, rusqlite::params![tx_hash, status, &serialize_async(tx).await,])?;
 
+        // Store its inverse
+        self.wallet.cache_inverse(inverse)?;
+
         Ok(tx_hash)
     }
 

+ 7 - 0
bin/drk/wallet.sql

@@ -9,6 +9,13 @@ CREATE TABLE IF NOT EXISTS wallet_info (
 	last_scanned_block_hash TEXT NOT NULL
 );
 
+-- Scanned blocks information
+CREATE TABLE IF NOT EXISTS scanned_blocks (
+	height INTEGER PRIMARY KEY NOT NULL,
+	hash TEXT NOT NULL,
+	rollback_query TEXT NOT NULL
+);
+
 -- Transactions history
 CREATE TABLE IF NOT EXISTS transactions_history (
     transaction_hash TEXT PRIMARY KEY NOT NULL,