diff --git a/src/database/sqlite/mod.rs b/src/database/sqlite/mod.rs index a425fd42..1e4a70fd 100644 --- a/src/database/sqlite/mod.rs +++ b/src/database/sqlite/mod.rs @@ -13,7 +13,7 @@ use crate::{ bitcoin::BlockChainTip, database::{ sqlite::{ - schema::{DbCoin, DbTip, DbWallet}, + schema::{DbAddress, DbCoin, DbTip, DbWallet}, utils::{create_fresh_db, db_exec, db_query}, }, Coin, @@ -23,7 +23,7 @@ use crate::{ use std::{convert::TryInto, fmt, io, path}; use miniscript::{ - bitcoin::{self, util::bip32}, + bitcoin::{self, secp256k1, util::bip32}, Descriptor, DescriptorPublicKey, }; @@ -93,10 +93,11 @@ impl SqliteDb { pub fn new( db_path: path::PathBuf, fresh_options: Option, + secp: &secp256k1::Secp256k1, ) -> Result { // Create the database if needed, and make sure the db file exists. if let Some(options) = fresh_options { - create_fresh_db(&db_path, options)?; + create_fresh_db(&db_path, options, secp)?; log::info!("Created a fresh database at {}.", db_path.display()); } if !db_path.exists() { @@ -291,6 +292,17 @@ impl SqliteConn { }) .expect("Database must be available") } + + pub fn db_address(&mut self, address: &bitcoin::Address) -> Option { + db_query( + &mut self.conn, + "SELECT * FROM addresses WHERE address = ?1", + rusqlite::params![address.to_string()], + |row| row.try_into(), + ) + .expect("Db must not fail") + .pop() + } } #[cfg(test)] @@ -300,9 +312,10 @@ mod tests { use std::{collections::HashSet, fs, path, str::FromStr}; use bitcoin::hashes::Hash; + use miniscript::{DescriptorTrait, TranslatePk2}; fn dummy_options() -> FreshDbOptions { - let desc_str = "wsh(andor(pk(03b506a1dbe57b4bf48c95e0c7d417b87dd3b4349d290d2e7e9ba72c912652d80a),older(10000),pk(0295e7f5d12a2061f1fd2286cefec592dff656a19f55f4f01305d6aa56630880ce)))"; + let desc_str = "wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/*)))#y5wcna2d"; let main_descriptor = Descriptor::::from_str(desc_str).unwrap(); FreshDbOptions { bitcoind_network: bitcoin::Network::Bitcoin, @@ -310,35 +323,42 @@ mod tests { } } - fn dummy_db() -> (path::PathBuf, FreshDbOptions, SqliteDb) { + fn dummy_db() -> ( + path::PathBuf, + FreshDbOptions, + secp256k1::Secp256k1, + SqliteDb, + ) { let tmp_dir = tmp_dir(); fs::create_dir_all(&tmp_dir).unwrap(); + let secp = secp256k1::Secp256k1::verification_only(); let db_path: path::PathBuf = [tmp_dir.as_path(), path::Path::new("minisafed.sqlite3")] .iter() .collect(); let options = dummy_options(); - let db = SqliteDb::new(db_path.clone(), Some(options.clone())).unwrap(); + let db = SqliteDb::new(db_path.clone(), Some(options.clone()), &secp).unwrap(); - (tmp_dir, options, db) + (tmp_dir, options, secp, db) } #[test] fn db_startup_sanity_checks() { let tmp_dir = tmp_dir(); fs::create_dir_all(&tmp_dir).unwrap(); + let secp = secp256k1::Secp256k1::verification_only(); let db_path: path::PathBuf = [tmp_dir.as_path(), path::Path::new("minisafed.sqlite3")] .iter() .collect(); - assert!(SqliteDb::new(db_path.clone(), None) + assert!(SqliteDb::new(db_path.clone(), None, &secp) .unwrap_err() .to_string() .contains("database file not found")); let options = dummy_options(); - let db = SqliteDb::new(db_path.clone(), Some(options.clone())).unwrap(); + let db = SqliteDb::new(db_path.clone(), Some(options.clone()), &secp).unwrap(); db.sanity_check(bitcoin::Network::Testnet, &options.main_descriptor) .unwrap_err() .to_string() @@ -346,7 +366,7 @@ mod tests { fs::remove_file(&db_path).unwrap(); let other_desc_str = "wsh(andor(pk(037a27a76ebf33594c785e4fa41607860a960bb5aa3039654297b05bff57e4f9a9),older(10000),pk(0295e7f5d12a2061f1fd2286cefec592dff656a19f55f4f01305d6aa56630880ce)))"; let other_desc = Descriptor::::from_str(other_desc_str).unwrap(); - let db = SqliteDb::new(db_path.clone(), Some(options.clone())).unwrap(); + let db = SqliteDb::new(db_path.clone(), Some(options.clone()), &secp).unwrap(); db.sanity_check(bitcoin::Network::Bitcoin, &other_desc) .unwrap_err() .to_string() @@ -354,10 +374,10 @@ mod tests { fs::remove_file(&db_path).unwrap(); // TODO: version check - let db = SqliteDb::new(db_path.clone(), Some(options.clone())).unwrap(); + let db = SqliteDb::new(db_path.clone(), Some(options.clone()), &secp).unwrap(); db.sanity_check(bitcoin::Network::Bitcoin, &options.main_descriptor) .unwrap(); - let db = SqliteDb::new(db_path.clone(), None).unwrap(); + let db = SqliteDb::new(db_path.clone(), None, &secp).unwrap(); db.sanity_check(bitcoin::Network::Bitcoin, &options.main_descriptor) .unwrap(); @@ -366,7 +386,7 @@ mod tests { #[test] fn db_tip_update() { - let (tmp_dir, options, db) = dummy_db(); + let (tmp_dir, options, _, db) = dummy_db(); { let mut conn = db.connection().unwrap(); @@ -394,7 +414,7 @@ mod tests { #[test] fn db_coins_update() { - let (tmp_dir, _, db) = dummy_db(); + let (tmp_dir, _, _, db) = dummy_db(); { let mut conn = db.connection().unwrap(); @@ -459,4 +479,47 @@ mod tests { fs::remove_dir_all(&tmp_dir).unwrap(); } + + #[test] + fn sqlite_addresses_cache() { + let (tmp_dir, options, secp, db) = dummy_db(); + + { + let mut conn = db.connection().unwrap(); + + // There is the index for the first index + let addr = options + .main_descriptor + .derive(0) + .translate_pk2(|xpk| xpk.derive_public_key(&secp)) + .expect("All pubkeys were derived, no wildcard.") + .address(options.bitcoind_network) + .expect("Always a P2WSH address"); + let db_addr = conn.db_address(&addr).unwrap(); + assert_eq!(db_addr.derivation_index, 0.into()); + + // There is the index for the 199th index (look-ahead limit) + let addr = options + .main_descriptor + .derive(199) + .translate_pk2(|xpk| xpk.derive_public_key(&secp)) + .expect("All pubkeys were derived, no wildcard.") + .address(options.bitcoind_network) + .expect("Always a P2WSH address"); + let db_addr = conn.db_address(&addr).unwrap(); + assert_eq!(db_addr.derivation_index, 199.into()); + + // And not for the 200th one. + let addr = options + .main_descriptor + .derive(200) + .translate_pk2(|xpk| xpk.derive_public_key(&secp)) + .expect("All pubkeys were derived, no wildcard.") + .address(options.bitcoind_network) + .expect("Always a P2WSH address"); + assert!(conn.db_address(&addr).is_none()); + } + + fs::remove_dir_all(&tmp_dir).unwrap(); + } } diff --git a/src/database/sqlite/schema.rs b/src/database/sqlite/schema.rs index 3d386415..1a0718aa 100644 --- a/src/database/sqlite/schema.rs +++ b/src/database/sqlite/schema.rs @@ -42,6 +42,14 @@ CREATE TABLE coins ( ON UPDATE RESTRICT ON DELETE RESTRICT ); + +/* A mapping from descriptor address to derivation index. Necessary until + * we can get the derivation index from the parent descriptor from bitcoind. + */ +CREATE TABLE addresses ( + address TEXT NOT NULL UNIQUE, + derivation_index INTEGER NOT NULL UNIQUE +); "; /// A row in the "tip" table. @@ -155,3 +163,27 @@ impl TryFrom<&rusqlite::Row<'_>> for DbCoin { }) } } + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DbAddress { + pub address: bitcoin::Address, + pub derivation_index: bip32::ChildNumber, +} + +impl TryFrom<&rusqlite::Row<'_>> for DbAddress { + type Error = rusqlite::Error; + + fn try_from(row: &rusqlite::Row) -> Result { + let address: String = row.get(0)?; + let address = bitcoin::Address::from_str(&address).expect("We only store valid addresses"); + + let derivation_index: u32 = row.get(1)?; + let derivation_index = bip32::ChildNumber::from(derivation_index); + assert!(derivation_index.is_normal()); + + Ok(DbAddress { + address, + derivation_index, + }) + } +} diff --git a/src/database/sqlite/utils.rs b/src/database/sqlite/utils.rs index 40b4f3f7..c42bdb76 100644 --- a/src/database/sqlite/utils.rs +++ b/src/database/sqlite/utils.rs @@ -2,6 +2,10 @@ use crate::database::sqlite::{schema::SCHEMA, FreshDbOptions, SqliteDbError, DB_ use std::{convert::TryInto, fs, path, time}; +use miniscript::{bitcoin::secp256k1, DescriptorTrait, TranslatePk2}; + +const LOOK_AHEAD_LIMIT: u32 = 200; + /// Perform a set of modifications to the database inside a single transaction pub fn db_exec(conn: &mut rusqlite::Connection, modifications: F) -> Result<(), rusqlite::Error> where @@ -62,7 +66,11 @@ pub fn create_db_file(db_path: &path::Path) -> Result<(), std::io::Error> { }; } -pub fn create_fresh_db(db_path: &path::Path, options: FreshDbOptions) -> Result<(), SqliteDbError> { +pub fn create_fresh_db( + db_path: &path::Path, + options: FreshDbOptions, + secp: &secp256k1::Secp256k1, +) -> Result<(), SqliteDbError> { create_db_file(db_path)?; let timestamp = time::SystemTime::now() @@ -70,6 +78,24 @@ pub fn create_fresh_db(db_path: &path::Path, options: FreshDbOptions) -> Result< .map(|dur| timestamp_to_u32(dur.as_secs())) .expect("System clock went backward the epoch?"); + // Fill the initial addresses. On a fresh database, the deposit_derivation_index is + // necessarily 0. + let mut query = String::with_capacity(100 * LOOK_AHEAD_LIMIT as usize); + for index in 0..LOOK_AHEAD_LIMIT { + // TODO: have this as a helper in descriptors.rs + let address = options + .main_descriptor + .derive(index) + .translate_pk2(|xpk| xpk.derive_public_key(secp)) + .expect("All pubkeys were derived, no wildcard.") + .address(options.bitcoind_network) + .expect("Always a P2WSH address"); + query += &format!( + "INSERT INTO addresses (address, derivation_index) VALUES (\"{}\", {});\n", + address, index + ); + } + let mut conn = rusqlite::Connection::open(db_path)?; db_exec(&mut conn, |tx| { tx.execute_batch(SCHEMA)?; @@ -86,6 +112,7 @@ pub fn create_fresh_db(db_path: &path::Path, options: FreshDbOptions) -> Result< VALUES (?1, ?2, ?3)", rusqlite::params![timestamp, options.main_descriptor.to_string(), 0,], )?; + tx.execute_batch(&query)?; Ok(()) })?; diff --git a/src/lib.rs b/src/lib.rs index e1a7431f..5d9ef901 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -161,6 +161,7 @@ fn setup_sqlite( config: &Config, data_dir: &path::Path, fresh_data_dir: bool, + secp: &secp256k1::Secp256k1, ) -> Result { let db_path: path::PathBuf = [data_dir, path::Path::new("minisafed.sqlite3")] .iter() @@ -173,7 +174,7 @@ fn setup_sqlite( } else { None }; - let sqlite = SqliteDb::new(db_path, options)?; + let sqlite = SqliteDb::new(db_path, options, secp)?; sqlite.sanity_check(config.bitcoin_config.network, &config.main_descriptor)?; log::info!("Database initialized and checked."); @@ -223,8 +224,8 @@ impl DaemonControl { config: Config, bitcoin: sync::Arc>, db: sync::Arc>, + secp: secp256k1::Secp256k1, ) -> DaemonControl { - let secp = secp256k1::Secp256k1::verification_only(); DaemonControl { config, bitcoin, @@ -257,6 +258,8 @@ impl DaemonHandle { #[cfg(not(test))] setup_panic_hook(); + let secp = secp256k1::Secp256k1::verification_only(); + // First, check the data directory let mut data_dir = config .data_dir() @@ -275,6 +278,7 @@ impl DaemonHandle { &config, &data_dir, fresh_data_dir, + &secp, )?)) as sync::Arc>, }; @@ -310,7 +314,7 @@ impl DaemonHandle { ); // Finally, set up the API. - let control = DaemonControl::new(config, bit, db); + let control = DaemonControl::new(config, bit, db, secp); Ok(Self { control,