diff --git a/src/bin/daemon.rs b/src/bin/daemon.rs index 8d188d7d..ea467ea7 100644 --- a/src/bin/daemon.rs +++ b/src/bin/daemon.rs @@ -58,10 +58,11 @@ fn main() { process::exit(1); }); - let _ = DaemonHandle::start(config).unwrap_or_else(|e| { + let daemon = DaemonHandle::start(config).unwrap_or_else(|e| { // The panic hook will log::error panic!("Starting Minisafe daemon: {}", e); }); + daemon.shutdown(); // We are always logging to stdout, should it be then piped to the log file (if self) or // not. So just make sure that all messages were actually written. diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 00000000..8069e7bb --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,58 @@ +//! # Minisafe commands +//! +//! External interface to the Minisafe daemon. + +use crate::{DaemonControl, VERSION}; + +use miniscript::{ + bitcoin, + descriptor::{self, DescriptorTrait}, + TranslatePk2, +}; + +impl DaemonControl { + /// Get information about the current state of the daemon + pub fn get_info(&self) -> GetInfoResult { + GetInfoResult { + version: VERSION.to_string(), + network: self.config.bitcoind_config.network, + blockheight: self.bitcoin.chain_tip().height, + sync: self.bitcoin.sync_progress(), + descriptors: GetInfoDescriptors { + main: self.config.main_descriptor.clone(), + }, + } + } + + /// Get a new deposit address. This will always generate a new deposit address, regardless of + /// whether it was actually used. + pub fn get_new_address(&self) -> bitcoin::Address { + let mut db_conn = self.db.connection(); + let index = db_conn.derivation_index(); + // TODO: handle should we wrap around instead of failing? + db_conn.update_derivation_index(index.increment().expect("TODO: handle wraparound")); + self.config + .main_descriptor + // TODO: have a descriptor newtype along with a derived descriptor one. + .derive(index.into()) + .translate_pk2(|xpk| xpk.derive_public_key(&self.secp)) + .expect("All pubkeys were derived, no wildcard.") + .address(self.config.bitcoind_config.network) + .expect("It's a wsh() descriptor") + } +} + +#[derive(Debug, Clone)] +pub struct GetInfoDescriptors { + pub main: descriptor::Descriptor, +} + +/// Information about the daemon +#[derive(Debug, Clone)] +pub struct GetInfoResult { + pub version: String, + pub network: bitcoin::Network, + pub blockheight: i32, + pub sync: f64, + pub descriptors: GetInfoDescriptors, +} diff --git a/src/database/mod.rs b/src/database/mod.rs index 80312209..dac70224 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -8,6 +8,8 @@ use crate::{ database::sqlite::{schema::DbTip, SqliteConn, SqliteDb}, }; +use miniscript::bitcoin::util::bip32; + pub trait DatabaseInterface: Send { fn connection(&self) -> Box; } @@ -24,6 +26,10 @@ pub trait DatabaseConnection { /// Update our best chain seen. fn update_tip(&mut self, tip: &BlockChainTip); + + fn derivation_index(&mut self) -> bip32::ChildNumber; + + fn update_derivation_index(&mut self, index: bip32::ChildNumber); } impl DatabaseConnection for SqliteConn { @@ -41,4 +47,12 @@ impl DatabaseConnection for SqliteConn { fn update_tip(&mut self, tip: &BlockChainTip) { self.update_tip(&tip) } + + fn derivation_index(&mut self) -> bip32::ChildNumber { + self.db_wallet().deposit_derivation_index + } + + fn update_derivation_index(&mut self, index: bip32::ChildNumber) { + self.update_derivation_index(index) + } } diff --git a/src/database/sqlite/mod.rs b/src/database/sqlite/mod.rs index c3bad332..1bc73aa2 100644 --- a/src/database/sqlite/mod.rs +++ b/src/database/sqlite/mod.rs @@ -19,7 +19,10 @@ use crate::{ use std::{convert::TryInto, fmt, io, path}; -use miniscript::{bitcoin, Descriptor, DescriptorPublicKey}; +use miniscript::{ + bitcoin::{self, util::bip32}, + Descriptor, DescriptorPublicKey, +}; const DB_VERSION: i64 = 0; @@ -196,6 +199,21 @@ impl SqliteConn { }) .expect("Database must be available") } + + /// Update the deposit derivation index. + pub fn update_derivation_index(&mut self, index: bip32::ChildNumber) { + let new_index: u32 = index.into(); + db_exec(&mut self.conn, |db_tx| { + // NOTE: should be updated if we ever have multi-wallet support + db_tx + .execute( + "UPDATE wallets SET deposit_derivation_index = (?1)", + rusqlite::params![new_index], + ) + .map(|_| ()) + }) + .expect("Database must be available") + } } #[cfg(test)] diff --git a/src/lib.rs b/src/lib.rs index 77e8b1bc..ba3da80a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ mod bitcoin; +mod commands; pub mod config; #[cfg(unix)] mod daemonize; @@ -10,14 +11,19 @@ pub use miniscript; use crate::{ bitcoin::{ d::{BitcoinD, BitcoindError}, - poller, + poller, BitcoinInterface, }, config::{config_folder_path, Config}, - database::sqlite::{FreshDbOptions, SqliteDb, SqliteDbError}, + database::{ + sqlite::{FreshDbOptions, SqliteDb, SqliteDbError}, + DatabaseInterface, + }, }; use std::{error, fmt, fs, io, path, sync}; +use miniscript::bitcoin::secp256k1; + #[cfg(not(test))] use std::{panic, process}; // A panic in any thread should stop the main thread, and print the panic. @@ -51,6 +57,20 @@ fn setup_panic_hook() { })); } +#[derive(Debug, Clone)] +pub struct Version { + pub major: u32, + pub minor: u32, +} + +impl fmt::Display for Version { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}.{}", self.major, self.minor) + } +} + +pub const VERSION: Version = Version { major: 0, minor: 1 }; + #[derive(Debug)] pub enum StartupError { Io(io::Error), @@ -124,7 +144,33 @@ fn create_datadir(datadir_path: &path::Path) -> Result<(), StartupError> { }; } -pub struct DaemonHandle {} +pub struct DaemonControl { + config: Config, + bitcoin: Box, + db: Box, + secp: secp256k1::Secp256k1, +} + +impl DaemonControl { + pub fn new( + config: Config, + bitcoin: Box, + db: Box, + ) -> DaemonControl { + let secp = secp256k1::Secp256k1::verification_only(); + DaemonControl { + config, + bitcoin, + db, + secp, + } + } +} + +pub struct DaemonHandle { + pub control: DaemonControl, + bitcoin_poller: poller::Poller, +} impl DaemonHandle { /// This starts the Minisafe daemon. Call `shutdown` to shut it down. @@ -138,6 +184,7 @@ impl DaemonHandle { // First, check the data directory let mut data_dir = config .data_dir + .clone() .unwrap_or(config_folder_path().ok_or(StartupError::DefaultDataDirNotFound)?); data_dir.push(config.bitcoind_config.network.to_string()); let fresh_data_dir = !data_dir.as_path().exists(); @@ -158,8 +205,8 @@ impl DaemonHandle { } else { None }; - let db = SqliteDb::new(db_path, options)?; - db.sanity_check(config.bitcoind_config.network, &config.main_descriptor)?; + let sqlite = SqliteDb::new(db_path, options)?; + sqlite.sanity_check(config.bitcoind_config.network, &config.main_descriptor)?; log::info!("Database initialized and checked."); // Now set up the bitcoind interface @@ -197,19 +244,26 @@ impl DaemonHandle { // Spawn the bitcoind poller with a retry limit high enough that we'd fail after that. let bitcoind = sync::Arc::from(sync::RwLock::from(bitcoind.with_retry_limit(None))); - let bit_poller = poller::Poller::start( + let bitcoin_poller = poller::Poller::start( bitcoind.clone(), - db, + sqlite.clone(), config.bitcoind_config.poll_interval_secs, ); - bit_poller.stop(); - Ok(Self {}) + // Finally, set up the API. + let control = DaemonControl::new(config, Box::from(bitcoind), Box::from(sqlite)); + + Ok(Self { + control, + bitcoin_poller, + }) } // NOTE: this moves out the data as it should not be reused after shutdown /// Shut down the Minisafe daemon. - pub fn shutdown(self) {} + pub fn shutdown(self) { + self.bitcoin_poller.stop(); + } } #[cfg(all(test, unix))] @@ -410,7 +464,7 @@ mod tests { }; // Create a dummy config with this bitcoind - let desc_str = "wsh(andor(pk(03b506a1dbe57b4bf48c95e0c7d417b87dd3b4349d290d2e7e9ba72c912652d80a),older(10000),pk(0295e7f5d12a2061f1fd2286cefec592dff656a19f55f4f01305d6aa56630880ce)))#39x77spy"; + let desc_str = "wsh(andor(pk(xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/*),older(10000),pk(xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/*)))#tk6wzexy"; let desc = Descriptor::::from_str(desc_str).unwrap(); let config = Config { bitcoind_config, @@ -426,7 +480,21 @@ mod tests { let config = config.clone(); move || { let handle = DaemonHandle::start(config).unwrap(); + // TODO: avoid scope creep. We should move the bitcoind-specific checks to the + // bitcoind module, test the startup with a mocked bitcoind interface, and not test + // commands here but in the commands module. + let addr = handle.control.get_new_address(); + let addr2 = handle.control.get_new_address(); + assert_eq!( + addr, + bitcoin::Address::from_str( + "bc1qdu9dama0pwc6fd9lj4sqzq4f728y5q2ucqyj55mfzfvuxr268zks7yajm3" + ) + .unwrap() + ); + assert_ne!(addr, addr2); handle.shutdown(); + addr } }); complete_sanity_check(&server); @@ -437,11 +505,13 @@ mod tests { complete_wallet_check(&server, &wo_path); complete_desc_check(&server, desc_str); complete_sync_check(&server); - daemon_thread.join().unwrap(); + let addr = daemon_thread.join().unwrap(); // The datadir is created now, so if we restart it it won't create the wo wallet. let daemon_thread = thread::spawn(move || { let handle = DaemonHandle::start(config).unwrap(); + // TODO: avoid scope creep. See above comment. + assert_ne!(handle.control.get_new_address(), addr); handle.shutdown(); }); complete_sanity_check(&server);