diff --git a/src/bin/cli.rs b/src/bin/cli.rs index 870bc271..38c6f569 100644 --- a/src/bin/cli.rs +++ b/src/bin/cli.rs @@ -93,7 +93,7 @@ fn socket_file(conf_file: Option) -> PathBuf { [ data_dir, - config.bitcoind_config.network.to_string().as_str(), + config.bitcoin_config.network.to_string().as_str(), "minisafed_rpc", ] .iter() diff --git a/src/commands/mod.rs b/src/commands/mod.rs index e56904e1..444b6d05 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -16,7 +16,7 @@ impl DaemonControl { pub fn get_info(&self) -> GetInfoResult { GetInfoResult { version: VERSION.to_string(), - network: self.config.bitcoind_config.network, + network: self.config.bitcoin_config.network, blockheight: self.bitcoin.chain_tip().height, sync: self.bitcoin.sync_progress(), descriptors: GetInfoDescriptors { @@ -39,7 +39,7 @@ impl DaemonControl { .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) + .address(self.config.bitcoin_config.network) .expect("It's a wsh() descriptor"); GetAddressResult { address } } diff --git a/src/config.rs b/src/config.rs index 42085b74..168746fc 100644 --- a/src/config.rs +++ b/src/config.rs @@ -54,13 +54,17 @@ fn default_daemon() -> bool { /// Everything we need to know for talking to bitcoind serenely #[derive(Debug, Clone, Deserialize, Serialize)] pub struct BitcoindConfig { - /// The network we are operating on, one of "bitcoin", "testnet", "regtest" - pub network: Network, /// Path to bitcoind's cookie file, to authenticate the RPC connection pub cookie_path: PathBuf, /// The IP:port bitcoind's RPC is listening on pub addr: SocketAddr, - /// The poll interval for bitcoind +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct BitcoinConfig { + /// The network we are operating on, one of "bitcoin", "testnet", "regtest", "signet" + pub network: Network, + /// The poll interval for the Bitcoin interface #[serde( deserialize_with = "deserialize_duration", serialize_with = "serialize_duration", @@ -91,8 +95,10 @@ pub struct Config { serialize_with = "serialize_to_string" )] pub main_descriptor: Descriptor, - /// Everything we need to know to talk to bitcoind - pub bitcoind_config: BitcoindConfig, + /// Settings for the Bitcoin interface + pub bitcoin_config: BitcoinConfig, + /// Settings specific to bitcoind as the Bitcoin interface + pub bitcoind_config: Option, } impl Config { @@ -195,7 +201,7 @@ impl Config { /// Make sure the settings are sane. pub fn check(&self) -> Result<(), ConfigError> { // Check the network of the xpubs in the descriptors - let expected_network = match self.bitcoind_config.network { + let expected_network = match self.bitcoin_config.network { Network::Bitcoin => Network::Bitcoin, _ => Network::Testnet, }; @@ -214,7 +220,7 @@ impl Config { if unexpected_net { return Err(ConfigError::Unexpected(format!( "Our bitcoin network is {} but one xpub is not for network {}", - self.bitcoind_config.network, expected_network + self.bitcoin_config.network, expected_network ))); } @@ -238,11 +244,13 @@ mod tests { log_level = "debug" main_descriptor = "wsh(andor(thresh(1,pk(xpub6BaZSKgpaVvibu2k78QsqeDWXp92xLHZxiu1WoqLB9hKhsBf3miBUDX7PJLgSPvkj66ThVHTqdnbXpeu8crXFmDUd4HeM4s4miQS2xsv3Qb/*)),and_v(v:multi(2,03b506a1dbe57b4bf48c95e0c7d417b87dd3b4349d290d2e7e9ba72c912652d80a,0295e7f5d12a2061f1fd2286cefec592dff656a19f55f4f01305d6aa56630880ce),older(4)),thresh(2,pkh(xpub6AHA9hZDN11k2ijHMeS5QqHx2KP9aMBRhTDqANMnwVtdyw2TDYRmF8PjpvwUFcL1Et8Hj59S3gTSMcUQ5gAqTz3Wd8EsMTmF3DChhqPQBnU/*),a:pkh(xpub6AaffFGfH6WXfm6pwWzmUMuECQnoLeB3agMKaLyEBZ5ZVfwtnS5VJKqXBt8o5ooCWVy2H87GsZshp7DeKE25eWLyd1Ccuh2ZubQUkgpiVux/*))))#532k8uvf" - [bitcoind_config] + [bitcoin_config] network = "bitcoin" + poll_interval_secs = 18 + + [bitcoind_config] cookie_path = "/home/user/.bitcoin/.cookie" addr = "127.0.0.1:8332" - poll_interval_secs = 18 "#.trim_start().replace(" ", ""); toml::from_str::(&toml_str).expect("Deserializing toml_str"); @@ -253,11 +261,13 @@ mod tests { log_level = 'TRACE' main_descriptor = 'wsh(andor(thresh(1,pk(xpub6BaZSKgpaVvibu2k78QsqeDWXp92xLHZxiu1WoqLB9hKhsBf3miBUDX7PJLgSPvkj66ThVHTqdnbXpeu8crXFmDUd4HeM4s4miQS2xsv3Qb/*)),and_v(v:multi(2,03b506a1dbe57b4bf48c95e0c7d417b87dd3b4349d290d2e7e9ba72c912652d80a,0295e7f5d12a2061f1fd2286cefec592dff656a19f55f4f01305d6aa56630880ce),older(4)),thresh(2,pkh(xpub6AHA9hZDN11k2ijHMeS5QqHx2KP9aMBRhTDqANMnwVtdyw2TDYRmF8PjpvwUFcL1Et8Hj59S3gTSMcUQ5gAqTz3Wd8EsMTmF3DChhqPQBnU/*),a:pkh(xpub6AaffFGfH6WXfm6pwWzmUMuECQnoLeB3agMKaLyEBZ5ZVfwtnS5VJKqXBt8o5ooCWVy2H87GsZshp7DeKE25eWLyd1Ccuh2ZubQUkgpiVux/*))))#532k8uvf' - [bitcoind_config] + [bitcoin_config] network = 'bitcoin' + poll_interval_secs = 18 + + [bitcoind_config] cookie_path = '/home/user/.bitcoin/.cookie' addr = '127.0.0.1:8332' - poll_interval_secs = 18 "#.trim_start().replace(" ", ""); let parsed = toml::from_str::(&toml_str).expect("Deserializing toml_str"); let serialized = toml::to_string_pretty(&parsed).expect("Serializing to toml"); @@ -273,16 +283,18 @@ mod tests { # The main descriptor semantics aren't checked, yet. main_descriptor = "wsh(andor(thresh(1,pk(xpub6BaZSKgpaVvibu2k78QsqeDWXp92xLHZxiu1WoqLB9hKhsBf3miBUDX7PJLgSPvkj66ThVHTqdnbXpeu8crXFmDUd4HeM4s4miQS2xsv3Qb/*)),and_v(v:multi(2,03b506a1dbe57b4bf48c95e0c7d417b87dd3b4349d290d2e7e9ba72c912652d80a,0295e7f5d12a2061f1fd2286cefec592dff656a19f55f4f01305d6aa56630880ce),older(4)),thresh(2,pkh(xpub6AHA9hZDN11k2ijHMeS5QqHx2KP9aMBRhTDqANMnwVtdyw2TDYRmF8PjpvwUFcL1Et8Hj59S3gTSMcUQ5gAqTz3Wd8EsMTmF3DChhqPQBnU/*),a:pkh(xpub6AaffFGfH6WXfm6pwWzmUMuECQnoLeB3agMKaLyEBZ5ZVfwtnS5VJKqXBt8o5ooCWVy2H87GsZshp7DeKE25eWLyd1Ccuh2ZubQUkgpiVux/*))))#532k88vf" - [bitcoind_config] + [bitcoin_config] network = "bitcoin" + poll_interval_secs = 18 + + [bitcoind_config] cookie_path = "/home/user/.bitcoin/.cookie" addr = "127.0.0.1:8332" - poll_interval_secs = 18 "#; let config_res: Result = toml::from_str(toml_str); config_res.expect_err("Deserializing an invalid toml_str"); - // Not enough parameters: missing the network + // Not enough parameters: missing the Bitcoin network let toml_str = r#" daemon = false log_level = "trace" @@ -291,10 +303,12 @@ mod tests { # The main descriptor semantics aren't checked, yet. main_descriptor = "wsh(andor(thresh(1,pk(xpub6BaZSKgpaVvibu2k78QsqeDWXp92xLHZxiu1WoqLB9hKhsBf3miBUDX7PJLgSPvkj66ThVHTqdnbXpeu8crXFmDUd4HeM4s4miQS2xsv3Qb/*)),and_v(v:multi(2,03b506a1dbe57b4bf48c95e0c7d417b87dd3b4349d290d2e7e9ba72c912652d80a,0295e7f5d12a2061f1fd2286cefec592dff656a19f55f4f01305d6aa56630880ce),older(4)),thresh(2,pkh(xpub6AHA9hZDN11k2ijHMeS5QqHx2KP9aMBRhTDqANMnwVtdyw2TDYRmF8PjpvwUFcL1Et8Hj59S3gTSMcUQ5gAqTz3Wd8EsMTmF3DChhqPQBnU/*),a:pkh(xpub6AaffFGfH6WXfm6pwWzmUMuECQnoLeB3agMKaLyEBZ5ZVfwtnS5VJKqXBt8o5ooCWVy2H87GsZshp7DeKE25eWLyd1Ccuh2ZubQUkgpiVux/*))))#532k8uvf" + [bitcoin_config] + poll_interval_secs = 18 + [bitcoind_config] cookie_path = "/home/user/.bitcoin/.cookie" addr = "127.0.0.1:8332" - poll_interval_secs = 18 "#; let config_res: Result = toml::from_str(toml_str); config_res.expect_err("Deserializing an invalid toml_str"); diff --git a/src/lib.rs b/src/lib.rs index 4e1f0a14..cac92090 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -80,6 +80,7 @@ pub enum StartupError { Io(io::Error), DefaultDataDirNotFound, DatadirCreation(path::PathBuf, io::Error), + MissingBitcoindConfig, Database(SqliteDbError), Bitcoind(BitcoindError), #[cfg(unix)] @@ -98,6 +99,10 @@ impl fmt::Display for StartupError { f, "Could not create data directory at '{}': '{}'", dir_path.display(), e ), + Self::MissingBitcoindConfig => write!( + f, + "Our Bitcoin interface is bitcoind but we have no 'bitcoind_config' entry in the configuration." + ), Self::Database(e) => write!(f, "Error initializing database: '{}'.", e), Self::Bitcoind(e) => write!(f, "Error setting up bitcoind interface: '{}'.", e), #[cfg(unix)] @@ -160,14 +165,14 @@ fn setup_sqlite( .collect(); let options = if fresh_data_dir { Some(FreshDbOptions { - bitcoind_network: config.bitcoind_config.network, + bitcoind_network: config.bitcoin_config.network, main_descriptor: config.main_descriptor.clone(), }) } else { None }; let sqlite = SqliteDb::new(db_path, options)?; - sqlite.sanity_check(config.bitcoind_config.network, &config.main_descriptor)?; + sqlite.sanity_check(config.bitcoin_config.network, &config.main_descriptor)?; log::info!("Database initialized and checked."); Ok(sqlite) @@ -185,7 +190,10 @@ fn setup_bitcoind( .iter() .collect(); let bitcoind = BitcoinD::new( - &config.bitcoind_config, + config + .bitcoind_config + .as_ref() + .ok_or(StartupError::MissingBitcoindConfig)?, wo_path.to_str().expect("Must be valid unicode").to_string(), )?; if fresh_data_dir { @@ -193,7 +201,7 @@ fn setup_bitcoind( log::info!("Created a new watchonly wallet on bitcoind."); } bitcoind.try_load_watchonly_wallet(); - bitcoind.sanity_check(&config.main_descriptor, config.bitcoind_config.network)?; + bitcoind.sanity_check(&config.main_descriptor, config.bitcoin_config.network)?; log::info!("Connection to bitcoind established and checked."); Ok(bitcoind.with_retry_limit(None)) @@ -250,7 +258,7 @@ impl DaemonHandle { let mut data_dir = config .data_dir() .ok_or(StartupError::DefaultDataDirNotFound)?; - data_dir.push(config.bitcoind_config.network.to_string()); + data_dir.push(config.bitcoin_config.network.to_string()); let fresh_data_dir = !data_dir.as_path().exists(); if fresh_data_dir { create_datadir(&data_dir)?; @@ -295,7 +303,7 @@ impl DaemonHandle { let bitcoin_poller = poller::Poller::start( bit.clone(), db.clone(), - config.bitcoind_config.poll_interval_secs, + config.bitcoin_config.poll_interval_secs, ); // Finally, set up the API. @@ -328,7 +336,7 @@ impl DaemonHandle { .data_dir() .expect("Didn't fail at startup, must not now") .as_path(), - path::Path::new(&control.config.bitcoind_config.network.to_string()), + path::Path::new(&control.config.bitcoin_config.network.to_string()), path::Path::new("minisafed_rpc"), ] .iter() @@ -356,7 +364,7 @@ impl DaemonHandle { #[cfg(all(test, unix))] mod tests { use super::*; - use crate::config::BitcoindConfig; + use crate::config::{BitcoinConfig, BitcoindConfig}; use miniscript::{bitcoin, Descriptor, DescriptorPublicKey}; use std::{ @@ -543,18 +551,21 @@ mod tests { net::SocketAddrV4::new(net::Ipv4Addr::new(127, 0, 0, 1), 0).into(); let server = net::TcpListener::bind(&addr).unwrap(); let addr = server.local_addr().unwrap(); - let bitcoind_config = BitcoindConfig { + let bitcoin_config = BitcoinConfig { network, + poll_interval_secs: time::Duration::from_secs(2), + }; + let bitcoind_config = BitcoindConfig { addr, cookie_path: cookie.clone(), - poll_interval_secs: time::Duration::from_secs(2), }; // Create a dummy config with this bitcoind 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, + bitcoin_config, + bitcoind_config: Some(bitcoind_config), data_dir: Some(data_dir.clone()), #[cfg(unix)] daemon: false, diff --git a/tests/test_framework/minisafed.py b/tests/test_framework/minisafed.py index 0b9135d0..7e365746 100644 --- a/tests/test_framework/minisafed.py +++ b/tests/test_framework/minisafed.py @@ -33,11 +33,13 @@ class Minisafed(TailableProc): f.write(f'main_descriptor = "{main_desc}"\n') - f.write("[bitcoind_config]\n") + f.write("[bitcoin_config]\n") f.write('network = "regtest"\n') + f.write("poll_interval_secs = 1\n") + + f.write("[bitcoind_config]\n") f.write(f"cookie_path = '{bitcoind_cookie_path}'\n") f.write(f"addr = '127.0.0.1:{bitcoind_rpc_port}'\n") - f.write("poll_interval_secs = 1\n") def start(self): TailableProc.start(self)