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,
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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(())
|
||||||
})?;
|
})?;
|
||||||
|
|||||||
10
src/lib.rs
10
src/lib.rs
@ -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,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user