sqlite: a table for a mapping from address to derivation index.

This commit is contained in:
Antoine Poinsot 2022-08-16 14:55:29 +02:00
parent c9ef068fa5
commit e74ea4c2d3
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304
4 changed files with 144 additions and 18 deletions

View File

@ -13,7 +13,7 @@ use crate::{
bitcoin::BlockChainTip, bitcoin::BlockChainTip,
database::{ database::{
sqlite::{ sqlite::{
schema::{DbCoin, DbTip, DbWallet}, schema::{DbAddress, DbCoin, DbTip, DbWallet},
utils::{create_fresh_db, db_exec, db_query}, utils::{create_fresh_db, db_exec, db_query},
}, },
Coin, Coin,
@ -23,7 +23,7 @@ use crate::{
use std::{convert::TryInto, fmt, io, path}; use std::{convert::TryInto, fmt, io, path};
use miniscript::{ use miniscript::{
bitcoin::{self, util::bip32}, bitcoin::{self, secp256k1, util::bip32},
Descriptor, DescriptorPublicKey, Descriptor, DescriptorPublicKey,
}; };
@ -93,10 +93,11 @@ impl SqliteDb {
pub fn new( pub fn new(
db_path: path::PathBuf, db_path: path::PathBuf,
fresh_options: Option<FreshDbOptions>, fresh_options: Option<FreshDbOptions>,
secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>,
) -> Result<SqliteDb, SqliteDbError> { ) -> Result<SqliteDb, SqliteDbError> {
// Create the database if needed, and make sure the db file exists. // Create the database if needed, and make sure the db file exists.
if let Some(options) = fresh_options { 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()); log::info!("Created a fresh database at {}.", db_path.display());
} }
if !db_path.exists() { if !db_path.exists() {
@ -291,6 +292,17 @@ impl SqliteConn {
}) })
.expect("Database must be available") .expect("Database must be available")
} }
pub fn db_address(&mut self, address: &bitcoin::Address) -> Option<DbAddress> {
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)] #[cfg(test)]
@ -300,9 +312,10 @@ mod tests {
use std::{collections::HashSet, fs, path, str::FromStr}; use std::{collections::HashSet, fs, path, str::FromStr};
use bitcoin::hashes::Hash; use bitcoin::hashes::Hash;
use miniscript::{DescriptorTrait, TranslatePk2};
fn dummy_options() -> FreshDbOptions { 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::<DescriptorPublicKey>::from_str(desc_str).unwrap(); let main_descriptor = Descriptor::<DescriptorPublicKey>::from_str(desc_str).unwrap();
FreshDbOptions { FreshDbOptions {
bitcoind_network: bitcoin::Network::Bitcoin, 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<secp256k1::VerifyOnly>,
SqliteDb,
) {
let tmp_dir = tmp_dir(); let tmp_dir = tmp_dir();
fs::create_dir_all(&tmp_dir).unwrap(); 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")] let db_path: path::PathBuf = [tmp_dir.as_path(), path::Path::new("minisafed.sqlite3")]
.iter() .iter()
.collect(); .collect();
let options = dummy_options(); 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] #[test]
fn db_startup_sanity_checks() { fn db_startup_sanity_checks() {
let tmp_dir = tmp_dir(); let tmp_dir = tmp_dir();
fs::create_dir_all(&tmp_dir).unwrap(); 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")] let db_path: path::PathBuf = [tmp_dir.as_path(), path::Path::new("minisafed.sqlite3")]
.iter() .iter()
.collect(); .collect();
assert!(SqliteDb::new(db_path.clone(), None) assert!(SqliteDb::new(db_path.clone(), None, &secp)
.unwrap_err() .unwrap_err()
.to_string() .to_string()
.contains("database file not found")); .contains("database file not found"));
let options = dummy_options(); 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) db.sanity_check(bitcoin::Network::Testnet, &options.main_descriptor)
.unwrap_err() .unwrap_err()
.to_string() .to_string()
@ -346,7 +366,7 @@ mod tests {
fs::remove_file(&db_path).unwrap(); fs::remove_file(&db_path).unwrap();
let other_desc_str = "wsh(andor(pk(037a27a76ebf33594c785e4fa41607860a960bb5aa3039654297b05bff57e4f9a9),older(10000),pk(0295e7f5d12a2061f1fd2286cefec592dff656a19f55f4f01305d6aa56630880ce)))"; let other_desc_str = "wsh(andor(pk(037a27a76ebf33594c785e4fa41607860a960bb5aa3039654297b05bff57e4f9a9),older(10000),pk(0295e7f5d12a2061f1fd2286cefec592dff656a19f55f4f01305d6aa56630880ce)))";
let other_desc = Descriptor::<DescriptorPublicKey>::from_str(other_desc_str).unwrap(); let other_desc = Descriptor::<DescriptorPublicKey>::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) db.sanity_check(bitcoin::Network::Bitcoin, &other_desc)
.unwrap_err() .unwrap_err()
.to_string() .to_string()
@ -354,10 +374,10 @@ mod tests {
fs::remove_file(&db_path).unwrap(); fs::remove_file(&db_path).unwrap();
// TODO: version check // 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) db.sanity_check(bitcoin::Network::Bitcoin, &options.main_descriptor)
.unwrap(); .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) db.sanity_check(bitcoin::Network::Bitcoin, &options.main_descriptor)
.unwrap(); .unwrap();
@ -366,7 +386,7 @@ mod tests {
#[test] #[test]
fn db_tip_update() { fn db_tip_update() {
let (tmp_dir, options, db) = dummy_db(); let (tmp_dir, options, _, db) = dummy_db();
{ {
let mut conn = db.connection().unwrap(); let mut conn = db.connection().unwrap();
@ -394,7 +414,7 @@ mod tests {
#[test] #[test]
fn db_coins_update() { fn db_coins_update() {
let (tmp_dir, _, db) = dummy_db(); let (tmp_dir, _, _, db) = dummy_db();
{ {
let mut conn = db.connection().unwrap(); let mut conn = db.connection().unwrap();
@ -459,4 +479,47 @@ mod tests {
fs::remove_dir_all(&tmp_dir).unwrap(); 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();
}
} }

View File

@ -42,6 +42,14 @@ CREATE TABLE coins (
ON UPDATE RESTRICT ON UPDATE RESTRICT
ON DELETE 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. /// 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<Self, Self::Error> {
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,
})
}
}

View File

@ -2,6 +2,10 @@ use crate::database::sqlite::{schema::SCHEMA, FreshDbOptions, SqliteDbError, DB_
use std::{convert::TryInto, fs, path, time}; 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 /// Perform a set of modifications to the database inside a single transaction
pub fn db_exec<F>(conn: &mut rusqlite::Connection, modifications: F) -> Result<(), rusqlite::Error> pub fn db_exec<F>(conn: &mut rusqlite::Connection, modifications: F) -> Result<(), rusqlite::Error>
where 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<secp256k1::VerifyOnly>,
) -> Result<(), SqliteDbError> {
create_db_file(db_path)?; create_db_file(db_path)?;
let timestamp = time::SystemTime::now() 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())) .map(|dur| timestamp_to_u32(dur.as_secs()))
.expect("System clock went backward the epoch?"); .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)?; let mut conn = rusqlite::Connection::open(db_path)?;
db_exec(&mut conn, |tx| { db_exec(&mut conn, |tx| {
tx.execute_batch(SCHEMA)?; tx.execute_batch(SCHEMA)?;
@ -86,6 +112,7 @@ pub fn create_fresh_db(db_path: &path::Path, options: FreshDbOptions) -> Result<
VALUES (?1, ?2, ?3)", VALUES (?1, ?2, ?3)",
rusqlite::params![timestamp, options.main_descriptor.to_string(), 0,], rusqlite::params![timestamp, options.main_descriptor.to_string(), 0,],
)?; )?;
tx.execute_batch(&query)?;
Ok(()) Ok(())
})?; })?;

View File

@ -161,6 +161,7 @@ fn setup_sqlite(
config: &Config, config: &Config,
data_dir: &path::Path, data_dir: &path::Path,
fresh_data_dir: bool, fresh_data_dir: bool,
secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>,
) -> Result<SqliteDb, StartupError> { ) -> Result<SqliteDb, StartupError> {
let db_path: path::PathBuf = [data_dir, path::Path::new("minisafed.sqlite3")] let db_path: path::PathBuf = [data_dir, path::Path::new("minisafed.sqlite3")]
.iter() .iter()
@ -173,7 +174,7 @@ fn setup_sqlite(
} else { } else {
None 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)?; sqlite.sanity_check(config.bitcoin_config.network, &config.main_descriptor)?;
log::info!("Database initialized and checked."); log::info!("Database initialized and checked.");
@ -223,8 +224,8 @@ impl DaemonControl {
config: Config, config: Config,
bitcoin: sync::Arc<sync::Mutex<dyn BitcoinInterface>>, bitcoin: sync::Arc<sync::Mutex<dyn BitcoinInterface>>,
db: sync::Arc<sync::Mutex<dyn DatabaseInterface>>, db: sync::Arc<sync::Mutex<dyn DatabaseInterface>>,
secp: secp256k1::Secp256k1<secp256k1::VerifyOnly>,
) -> DaemonControl { ) -> DaemonControl {
let secp = secp256k1::Secp256k1::verification_only();
DaemonControl { DaemonControl {
config, config,
bitcoin, bitcoin,
@ -257,6 +258,8 @@ impl DaemonHandle {
#[cfg(not(test))] #[cfg(not(test))]
setup_panic_hook(); setup_panic_hook();
let secp = secp256k1::Secp256k1::verification_only();
// First, check the data directory // First, check the data directory
let mut data_dir = config let mut data_dir = config
.data_dir() .data_dir()
@ -275,6 +278,7 @@ impl DaemonHandle {
&config, &config,
&data_dir, &data_dir,
fresh_data_dir, fresh_data_dir,
&secp,
)?)) as sync::Arc<sync::Mutex<dyn DatabaseInterface>>, )?)) as sync::Arc<sync::Mutex<dyn DatabaseInterface>>,
}; };
@ -310,7 +314,7 @@ impl DaemonHandle {
); );
// Finally, set up the API. // Finally, set up the API.
let control = DaemonControl::new(config, bit, db); let control = DaemonControl::new(config, bit, db, secp);
Ok(Self { Ok(Self {
control, control,