sqlite: a table for a mapping from address to derivation index.
This commit is contained in:
parent
c9ef068fa5
commit
e74ea4c2d3
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(())
|
||||
})?;
|
||||
|
||||
10
src/lib.rs
10
src/lib.rs
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user