diff --git a/src/database/mod.rs b/src/database/mod.rs index 20795bb1..263f3442 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -53,6 +53,15 @@ pub trait DatabaseConnection { fn increment_change_index(&mut self, secp: &secp256k1::Secp256k1); + /// Get the timestamp at which to start rescaning from, if any. + fn rescan_timestamp(&mut self) -> Option; + + /// Set a timestamp at which to start rescaning the block chain from. + fn set_rescan(&mut self, timestamp: u32); + + /// Mark the rescan as complete. + fn complete_rescan(&mut self); + /// Get the derivation index for this address, as well as whether this address is change. fn derivation_index_by_address( &mut self, @@ -134,6 +143,18 @@ impl DatabaseConnection for SqliteConn { self.increment_change_index(secp) } + fn rescan_timestamp(&mut self) -> Option { + self.db_wallet().rescan_timestamp + } + + fn set_rescan(&mut self, timestamp: u32) { + self.set_wallet_rescan_timestamp(timestamp) + } + + fn complete_rescan(&mut self) { + self.complete_wallet_rescan() + } + fn coins(&mut self) -> HashMap { self.coins() .into_iter() diff --git a/src/database/sqlite/mod.rs b/src/database/sqlite/mod.rs index dc15827b..c942eef5 100644 --- a/src/database/sqlite/mod.rs +++ b/src/database/sqlite/mod.rs @@ -21,7 +21,7 @@ use crate::{ descriptors::MultipathDescriptor, }; -use std::{convert::TryInto, fmt, io, path}; +use std::{cmp, convert::TryInto, fmt, io, path}; use miniscript::bitcoin::{ self, consensus::encode, hashes::hex::ToHex, secp256k1, @@ -272,6 +272,43 @@ impl SqliteConn { .expect("Database must be available") } + pub fn set_wallet_rescan_timestamp(&mut self, timestamp: u32) { + db_exec(&mut self.conn, |db_tx| { + // NOTE: this will need to be updated if we ever implement multi-wallet support + db_tx + .execute( + "UPDATE wallets SET rescan_timestamp = (?1)", + rusqlite::params![timestamp], + ) + .map(|_| ()) + }) + .expect("Database must be available") + } + + /// Drop the rescan timestamp, and set it as the wallet creation timestamp if it + /// predates it. + /// + /// # Panics + /// - If called while rescan_timestamp is not set + pub fn complete_wallet_rescan(&mut self) { + let db_wallet = self.db_wallet(); + let new_timestamp = cmp::min( + db_wallet.rescan_timestamp.expect("Must be set"), + db_wallet.timestamp, + ); + + db_exec(&mut self.conn, |db_tx| { + // NOTE: this will need to be updated if we ever implement multi-wallet support + db_tx + .execute( + "UPDATE wallets SET timestamp = (?1), rescan_timestamp = NULL", + rusqlite::params![new_timestamp], + ) + .map(|_| ()) + }) + .expect("Database must be available"); + } + /// Get all the coins from DB. pub fn coins(&mut self) -> Vec { db_query( @@ -998,4 +1035,42 @@ mod tests { fs::remove_dir_all(&tmp_dir).unwrap(); } + + #[test] + fn db_rescan() { + let (tmp_dir, _, _, db) = dummy_db(); + + { + let mut conn = db.connection().unwrap(); + + // At first no rescan is ongoing + let dummy_timestamp = 1_001; + let db_wallet = conn.db_wallet(); + assert!(db_wallet.rescan_timestamp.is_none()); + assert!(db_wallet.timestamp > dummy_timestamp); + + // But if we set one there'll be + conn.set_wallet_rescan_timestamp(dummy_timestamp); + assert_eq!(conn.db_wallet().rescan_timestamp, Some(dummy_timestamp)); + + // Once it's done the rescan timestamp will be erased, and the + // wallet timestamp will be set to the dummy timestamp since it's + // lower. + conn.complete_wallet_rescan(); + let db_wallet = conn.db_wallet(); + assert!(db_wallet.rescan_timestamp.is_none()); + assert_eq!(db_wallet.timestamp, dummy_timestamp); + + // If we rescan from a later timestamp, we'll keep the existing + // wallet timestamp afterward. + conn.set_wallet_rescan_timestamp(dummy_timestamp + 1); + assert_eq!(conn.db_wallet().rescan_timestamp, Some(dummy_timestamp + 1)); + conn.complete_wallet_rescan(); + let db_wallet = conn.db_wallet(); + assert!(db_wallet.rescan_timestamp.is_none()); + assert_eq!(db_wallet.timestamp, dummy_timestamp); + } + + fs::remove_dir_all(&tmp_dir).unwrap(); + } } diff --git a/src/database/sqlite/schema.rs b/src/database/sqlite/schema.rs index 40398df7..a4a51feb 100644 --- a/src/database/sqlite/schema.rs +++ b/src/database/sqlite/schema.rs @@ -22,13 +22,19 @@ CREATE TABLE tip ( /* This stores metadata about our wallet. We only support single wallet for * now (and the foreseeable future). + * + * The 'timestamp' field is the creation date of the wallet. We guarantee to have seen all + * information related to our descriptor(s) that occured after this date. + * The optional 'rescan_timestamp' field is a the timestamp we need to rescan the chain + * for events related to our descriptor(s) from. */ CREATE TABLE wallets ( id INTEGER PRIMARY KEY NOT NULL, timestamp INTEGER NOT NULL, main_descriptor TEXT NOT NULL, deposit_derivation_index INTEGER NOT NULL, - change_derivation_index INTEGER NOT NULL + change_derivation_index INTEGER NOT NULL, + rescan_timestamp INTEGER ); /* Our (U)TxOs. @@ -109,6 +115,7 @@ pub struct DbWallet { pub main_descriptor: MultipathDescriptor, pub deposit_derivation_index: bip32::ChildNumber, pub change_derivation_index: bip32::ChildNumber, + pub rescan_timestamp: Option, } impl TryFrom<&rusqlite::Row<'_>> for DbWallet { @@ -127,12 +134,15 @@ impl TryFrom<&rusqlite::Row<'_>> for DbWallet { let der_idx: u32 = row.get(4)?; let change_derivation_index = bip32::ChildNumber::from(der_idx); + let rescan_timestamp = row.get(5)?; + Ok(DbWallet { id, timestamp, main_descriptor, deposit_derivation_index, change_derivation_index, + rescan_timestamp, }) } } diff --git a/src/testutils.rs b/src/testutils.rs index 355e8bc3..6201f32b 100644 --- a/src/testutils.rs +++ b/src/testutils.rs @@ -266,6 +266,18 @@ impl DatabaseConnection for DummyDbConn { fn rollback_tip(&mut self, _: &BlockChainTip) { todo!() } + + fn rescan_timestamp(&mut self) -> Option { + None + } + + fn set_rescan(&mut self, _: u32) { + todo!() + } + + fn complete_rescan(&mut self) { + todo!() + } } pub struct DummyMinisafe {