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,
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<FreshDbOptions>,
secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>,
) -> Result<SqliteDb, SqliteDbError> {
// 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<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)]
@ -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::<DescriptorPublicKey>::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<secp256k1::VerifyOnly>,
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::<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)
.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();
}
}

View File

@ -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<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 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<F>(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<secp256k1::VerifyOnly>,
) -> 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(())
})?;

View File

@ -161,6 +161,7 @@ fn setup_sqlite(
config: &Config,
data_dir: &path::Path,
fresh_data_dir: bool,
secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>,
) -> Result<SqliteDb, StartupError> {
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<sync::Mutex<dyn BitcoinInterface>>,
db: sync::Arc<sync::Mutex<dyn DatabaseInterface>>,
secp: secp256k1::Secp256k1<secp256k1::VerifyOnly>,
) -> 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<sync::Mutex<dyn DatabaseInterface>>,
};
@ -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,