db: the interface to store the state of an ongoing rescan

We'll need to store in persistent storage if a rescan was requested by a
user, and if so from what date.

For the SQLite implementation we introduce a rescan_timestamp to the
wallet table.
This commit is contained in:
Antoine Poinsot 2022-11-09 18:16:55 +01:00
parent bd4de0b87a
commit 7e83bfad55
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304
4 changed files with 120 additions and 2 deletions

View File

@ -53,6 +53,15 @@ pub trait DatabaseConnection {
fn increment_change_index(&mut self, secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>);
/// Get the timestamp at which to start rescaning from, if any.
fn rescan_timestamp(&mut self) -> Option<u32>;
/// 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<u32> {
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<bitcoin::OutPoint, Coin> {
self.coins()
.into_iter()

View File

@ -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<DbCoin> {
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();
}
}

View File

@ -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<u32>,
}
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,
})
}
}

View File

@ -266,6 +266,18 @@ impl DatabaseConnection for DummyDbConn {
fn rollback_tip(&mut self, _: &BlockChainTip) {
todo!()
}
fn rescan_timestamp(&mut self) -> Option<u32> {
None
}
fn set_rescan(&mut self, _: u32) {
todo!()
}
fn complete_rescan(&mut self) {
todo!()
}
}
pub struct DummyMinisafe {