diff --git a/src/daemon/bitcoind/interface.rs b/src/daemon/bitcoind/interface.rs index ac07524c..bf24c290 100644 --- a/src/daemon/bitcoind/interface.rs +++ b/src/daemon/bitcoind/interface.rs @@ -33,10 +33,12 @@ const RPC_SOCKET_TIMEOUT: u64 = 180; // Labels used to tag utxos in the watchonly wallet const DEPOSIT_UTXOS_LABEL: &str = "revault-deposit"; const UNVAULT_UTXOS_LABEL: &str = "revault-unvault"; +const CPFP_UTXOS_LABEL: &str = "revault-cpfp"; pub struct BitcoinD { node_client: Client, watchonly_client: Client, + cpfp_client: Client, } macro_rules! params { @@ -53,6 +55,7 @@ impl BitcoinD { pub fn new( config: &BitcoindConfig, watchonly_wallet_path: String, + cpfp_wallet_path: String, ) -> Result { let cookie_string = fs::read_to_string(&config.cookie_path).map_err(|e| { BitcoindError::Custom(format!("Reading cookie file: {}", e.to_string())) @@ -67,10 +70,20 @@ impl BitcoinD { .build(), ); - let url = format!("http://{}/wallet/{}", config.addr, watchonly_wallet_path); + let watchonly_url = format!("http://{}/wallet/{}", config.addr, watchonly_wallet_path); let watchonly_client = Client::with_transport( SimpleHttpTransport::builder() - .url(&url) + .url(&watchonly_url) + .map_err(BitcoindError::from)? + .timeout(Duration::from_secs(RPC_SOCKET_TIMEOUT)) + .cookie_auth(cookie_string.clone()) + .build(), + ); + + let cpfp_url = format!("http://{}/wallet/{}", config.addr, cpfp_wallet_path); + let cpfp_client = Client::with_transport( + SimpleHttpTransport::builder() + .url(&cpfp_url) .map_err(BitcoindError::from)? .timeout(Duration::from_secs(RPC_SOCKET_TIMEOUT)) .cookie_auth(cookie_string) @@ -80,6 +93,7 @@ impl BitcoinD { Ok(BitcoinD { node_client, watchonly_client, + cpfp_client, }) } @@ -230,6 +244,14 @@ impl BitcoinD { self.make_requests(&self.node_client, requests) } + fn make_cpfp_request<'a, 'b>( + &self, + method: &'a str, + params: &'b [Box], + ) -> Result { + self.make_request(&self.cpfp_client, method, params) + } + pub fn getblockchaininfo(&self) -> Result { self.make_node_request("getblockchaininfo", &[]) } @@ -297,13 +319,17 @@ impl BitcoinD { }) } - pub fn createwallet_startup(&self, wallet_path: String) -> Result<(), BitcoindError> { + pub fn createwallet_startup( + &self, + wallet_path: String, + watchonly: bool, + ) -> Result<(), BitcoindError> { let res = self.make_node_request( "createwallet", ¶ms!( Json::String(wallet_path), - Json::Bool(true), // watchonly - Json::Bool(false), // blank + Json::Bool(watchonly), // watchonly + Json::Bool(true), // blank Json::String("".to_string()), // passphrase, Json::Bool(false), // avoid_reuse Json::Bool(true), // descriptors @@ -406,11 +432,17 @@ impl BitcoinD { fn bulk_import_descriptors( &self, + client: &Client, descriptors: Vec, timestamp: u32, label: String, fresh_wallet: bool, + active: bool, ) -> Result<(), BitcoindError> { + if !fresh_wallet { + log::debug!("Not a fresh wallet, rescan *may* take some time."); + } + let all_descriptors: Vec = descriptors .into_iter() .map(|desc| { @@ -423,25 +455,28 @@ impl BitcoinD { if fresh_wallet { Json::String("now".to_string()) } else { - log::debug!("Not a fresh wallet, rescan *may* take some time."); Json::Number(serde_json::Number::from(timestamp)) }, ); desc_map.insert("label".to_string(), Json::String(label.clone())); + desc_map.insert("active".to_string(), Json::Bool(active)); Json::Object(desc_map) }) .collect(); - let res = self - .make_watchonly_request("importdescriptors", ¶ms!(Json::Array(all_descriptors)))?; + let res = self.make_request( + &client, + "importdescriptors", + ¶ms!(Json::Array(all_descriptors)), + )?; if res.get(0).map(|x| x.get("success")) == Some(Some(&Json::Bool(true))) { return Ok(()); } Err(BitcoindError::Custom(format!( "Error returned from 'importdescriptor': {:?}", - res.get("error") + res.get(0).map(|r| r.get("error")) ))) } @@ -452,10 +487,12 @@ impl BitcoinD { fresh_wallet: bool, ) -> Result<(), BitcoindError> { self.bulk_import_descriptors( + &self.watchonly_client, descriptors, timestamp, DEPOSIT_UTXOS_LABEL.to_string(), fresh_wallet, + false, ) } @@ -466,10 +503,28 @@ impl BitcoinD { fresh_wallet: bool, ) -> Result<(), BitcoindError> { self.bulk_import_descriptors( + &self.watchonly_client, descriptors, timestamp, UNVAULT_UTXOS_LABEL.to_string(), fresh_wallet, + false, + ) + } + + pub fn startup_import_cpfp_descriptor( + &self, + descriptor: String, + timestamp: u32, + fresh_wallet: bool, + ) -> Result<(), BitcoindError> { + self.bulk_import_descriptors( + &self.cpfp_client, + vec![descriptor], + timestamp, + CPFP_UTXOS_LABEL.to_string(), + fresh_wallet, + true, ) } diff --git a/src/daemon/bitcoind/mod.rs b/src/daemon/bitcoind/mod.rs index d327c233..fa678ee7 100644 --- a/src/daemon/bitcoind/mod.rs +++ b/src/daemon/bitcoind/mod.rs @@ -126,6 +126,9 @@ pub fn start_bitcoind(revaultd: &mut RevaultD) -> Result Result<( } } - bitcoind.createwallet_startup(bitcoind_wallet_path)?; + bitcoind.createwallet_startup(bitcoind_wallet_path, true)?; log::info!("Importing descriptors to bitcoind watchonly wallet."); // Now, import descriptors. @@ -1539,6 +1540,48 @@ fn maybe_create_wallet(revaultd: &mut RevaultD, bitcoind: &BitcoinD) -> Result<( bitcoind.startup_import_unvault_descriptors(addresses, wallet.timestamp, fresh_wallet)?; } + if let Some(cpfp_key) = revaultd.cpfp_key { + let cpfp_wallet_path = revaultd + .cpfp_wallet_file() + .expect("Wallet id is set at startup in setup_db()"); + + if !PathBuf::from(cpfp_wallet_path.clone()).exists() { + log::info!("Creating the CPFP wallet"); + // Remove any leftover. This can happen if we delete the cpfp wallet but don't restart + // bitcoind. + while bitcoind.listwallets()?.contains(&cpfp_wallet_path) { + log::info!("Found a leftover cpfp wallet loaded on bitcoind. Removing it."); + if let Err(e) = bitcoind.unloadwallet(cpfp_wallet_path.clone()) { + log::error!("Unloading wallet '{}': '{}'", &cpfp_wallet_path, e); + } + } + + bitcoind.createwallet_startup(cpfp_wallet_path, false)?; + log::info!("Importing descriptors to bitcoind cpfp wallet."); + + // Now, import descriptors. + let mut keymap: KeyMap = KeyMap::new(); + let cpfp_private_key = DescriptorSecretKey::XPrv(DescriptorXKey { + xkey: cpfp_key.clone(), + origin: None, + derivation_path: Default::default(), + wildcard: Wildcard::Unhardened, + }); + let sign_ctx = Secp256k1::signing_only(); + let cpfp_public_key = cpfp_private_key + .as_public(&sign_ctx) + .expect("We never use hardened"); + keymap.insert(cpfp_public_key, cpfp_private_key); + let cpfp_desc = revaultd + .cpfp_descriptor + .inner() + .to_string_with_secret(&keymap); + + bitcoind.startup_import_cpfp_descriptor(cpfp_desc, wallet.timestamp, fresh_wallet)?; + } + } else { + log::info!("Not creating the CPFP wallet, as we don't have a CPFP key"); + } Ok(()) } diff --git a/src/daemon/revaultd.rs b/src/daemon/revaultd.rs index 949b655a..13b2d797 100644 --- a/src/daemon/revaultd.rs +++ b/src/daemon/revaultd.rs @@ -19,7 +19,7 @@ use revault_net::{ use revault_tx::{ bitcoin::{ secp256k1, - util::bip32::{ChildNumber, ExtendedPubKey}, + util::bip32::{ChildNumber, ExtendedPrivKey, ExtendedPubKey}, Address, BlockHash, PublicKey as BitcoinPublicKey, Script, TxOut, }, miniscript::descriptor::{DescriptorPublicKey, DescriptorTrait}, @@ -155,12 +155,12 @@ impl fmt::Display for VaultStatus { // An error related to the initialization of communication keys #[derive(Debug)] -enum KeyError { +enum NoiseKeyError { ReadingKey(io::Error), WritingKey(io::Error), } -impl fmt::Display for KeyError { +impl fmt::Display for NoiseKeyError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::ReadingKey(e) => write!(f, "Error reading Noise key: '{}'", e), @@ -169,10 +169,35 @@ impl fmt::Display for KeyError { } } -impl std::error::Error for KeyError {} +impl std::error::Error for NoiseKeyError {} + +#[derive(Debug)] +enum CpfpKeyError { + ReadingKey(io::Error), + InvalidKey(String), + KeyNotInDescriptor(String), +} + +impl fmt::Display for CpfpKeyError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::ReadingKey(e) => write!(f, "Error reading CPFP key: '{}'", e), + Self::InvalidKey(e) => write!(f, "Invalid CPFP key: '{}'", e), + Self::KeyNotInDescriptor(s) => { + write!( + f, + "The key provided is not present in the CPFP descriptor: {}", + s + ) + } + } + } +} + +impl std::error::Error for CpfpKeyError {} // The communication keys are (for now) hot, so we just create it ourselves on first run. -fn read_or_create_noise_key(secret_file: PathBuf) -> Result { +fn read_or_create_noise_key(secret_file: PathBuf) -> Result { let mut noise_secret = NoisePrivKey([0; 32]); if !secret_file.as_path().exists() { @@ -192,14 +217,16 @@ fn read_or_create_noise_key(secret_file: PathBuf) -> Result Result Result, CpfpKeyError> { + // No file? No key :) + let mut cpfp_secret_fd = match fs::File::open(secret_file) { + Ok(fd) => fd, + Err(_) => return Ok(None), + }; + + let mut buffer = [0; 78]; + cpfp_secret_fd + .read_exact(&mut buffer) + .map_err(CpfpKeyError::ReadingKey)?; + ExtendedPrivKey::decode(&buffer) + .map(Option::Some) + .map_err(|e| CpfpKeyError::InvalidKey(e.to_string())) +} + /// A vault is defined as a confirmed utxo paying to the Vault Descriptor for which /// we have a set of pre-signed transaction (emergency, cancel, unvault). /// Depending on its status we may not yet be in possession of part -or the entirety- @@ -260,6 +303,10 @@ pub struct RevaultD { pub secp_ctx: secp256k1::Secp256k1, /// The locktime to use on all created transaction. Always 0 for now. pub lock_time: u32, + /// CPFP private key to fee-bump Unvault and Spend transactions. + /// In the absence of the key file in the data directory, the automated CPFP mechanism + /// is silently disabled. + pub cpfp_key: Option, // Network stuff /// The static private key we use to establish connections to servers. We reuse it, but Trevor @@ -345,6 +392,35 @@ impl RevaultD { let noise_secret_file = [data_dir_str, "noise_secret"].iter().collect(); let noise_secret = read_or_create_noise_key(noise_secret_file)?; + let cpfp_key = if our_man_xpub.is_some() { + let cpfp_key_file = [data_dir_str, "cpfp_secret"].iter().collect(); + let key = read_cpfp_key(cpfp_key_file)?; + if let Some(key) = key { + // Checking if the key is in the cpfp descriptor + let secp_ctx = secp256k1::Secp256k1::signing_only(); + let pubkey = ExtendedPubKey::from_private(&secp_ctx, &key); + cpfp_descriptor + .xpubs() + .iter() + .find(|k| { + if let DescriptorPublicKey::XPub(k) = k { + k.xkey == pubkey + } else { + unreachable!(); + } + }) + .ok_or(CpfpKeyError::KeyNotInDescriptor(pubkey.to_string()))?; + } else { + log::warn!( + "CPFP key not found, consider creating a cpfp_secret file in the datadir. \ + Automated CPFP won't be available." + ); + } + key + } else { + None + }; + // TODO: support hidden services let coordinator_host = SocketAddr::from_str(&config.coordinator_host)?; let coordinator_noisekey = config.coordinator_noise_key; @@ -387,6 +463,7 @@ impl RevaultD { cosigs, watchtowers, lock_time: 0, + cpfp_key, min_conf: config.min_conf, bitcoind_config: config.bitcoind_config, tip: None, @@ -430,6 +507,14 @@ impl RevaultD { .expect("unvault_descriptor is a wsh") } + pub fn cpfp_address(&self, child_number: ChildNumber) -> Address { + self.cpfp_descriptor + .derive(child_number, &self.secp_ctx) + .inner() + .address(self.bitcoind_config.network) + .expect("cpfp_descriptor is a wsh") + } + pub fn gap_limit(&self) -> u32 { 100 } @@ -439,6 +524,11 @@ impl RevaultD { .map(|ref id| format!("revaultd-watchonly-wallet-{}", id)) } + pub fn cpfp_wallet_name(&self) -> Option { + self.wallet_id + .map(|ref id| format!("revaultd-cpfp-wallet-{}", id)) + } + pub fn log_file(&self) -> PathBuf { self.file_from_datadir("log") } @@ -460,6 +550,15 @@ impl RevaultD { }) } + pub fn cpfp_wallet_file(&self) -> Option { + self.cpfp_wallet_name().map(|ref name| { + self.file_from_datadir(name) + .to_str() + .expect("Valid utf-8") + .to_string() + }) + } + pub fn rpc_socket_file(&self) -> PathBuf { self.file_from_datadir("revaultd_rpc") }