diff --git a/src/bitcoin/d/mod.rs b/src/bitcoin/d/mod.rs index f7d74293..7dea4d14 100644 --- a/src/bitcoin/d/mod.rs +++ b/src/bitcoin/d/mod.rs @@ -1,7 +1,9 @@ ///! Implementation of the Bitcoin interface using bitcoind. ///! ///! We use the RPC interface and a watchonly descriptor wallet. +mod utils; use crate::{bitcoin::BlockChainTip, config, descriptors::MultipathDescriptor}; +use utils::block_before_date; use std::{collections::HashSet, convert::TryInto, fs, io, str::FromStr, time::Duration}; @@ -391,19 +393,34 @@ impl BitcoinD { } // Import the receive and change descriptors from the multipath descriptor to bitcoind. - fn import_descriptor(&self, desc: &MultipathDescriptor) -> Option { + // An optional timestamp may be given to rescan the chain from this date for this descriptor. + fn import_descriptor( + &self, + desc: &MultipathDescriptor, + timestamp: Option, + ) -> Option { let descriptors = [desc.receive_descriptor(), desc.change_descriptor()] .iter() .map(|desc| { - // TODO: rescan feature will probably need another timestamp than 'now' serde_json::json!({ "desc": desc.to_string(), - "timestamp": "now", + "timestamp": timestamp.map(Json::from).unwrap_or_else(|| "now".into()), "active": false, }) }) .collect(); + // If this will trigger a rescan, do not wait for the response. + if timestamp.is_some() { + // TODO: should we check there was not timeout when writing the request on the + // TcpStream in the SimpleHttpTransport implem? + // NOTE: if the rescan gets aborted through the 'abortrescan' RPC we won't see the + // error and bitcoind will keep the new timestamps for the descriptors as if it had + // successfully rescanned them. + self.make_noreply_request("importdescriptors", ¶ms!(Json::Array(descriptors))); + return None; + } + let res = self.make_wallet_request("importdescriptors", ¶ms!(Json::Array(descriptors))); let all_succeeded = res .as_array() @@ -459,7 +476,7 @@ impl BitcoinD { if let Some(err) = self.create_wallet(self.watchonly_wallet_path.clone()) { return Err(BitcoindError::WalletCreation(err)); } - if let Some(err) = self.import_descriptor(main_descriptor) { + if let Some(err) = self.import_descriptor(main_descriptor, None) { return Err(BitcoindError::DescriptorImport(err)); } @@ -754,6 +771,30 @@ impl BitcoinD { )?; Ok(()) } + + pub fn start_rescan(&self, desc: &MultipathDescriptor, timestamp: u32) { + self.import_descriptor(desc, Some(timestamp)); + } + + /// Get the progress of the ongoing rescan, if there is any. + pub fn rescan_progress(&self) -> Option { + self.make_wallet_request("getwalletinfo", &[]) + .get("scanning") + // If no rescan is ongoing, it will fail cause it would be 'false' + .and_then(Json::as_object) + .and_then(|map| map.get("progress")) + .and_then(Json::as_f64) + } + + /// Get the height and hash of the last block with a timestamp below the given one. + pub fn tip_before_timestamp(&self, timestamp: u32) -> Option { + block_before_date( + timestamp, + self.chain_tip(), + |h| self.get_block_hash(h), + |h| self.get_block_stats(h), + ) + } } // Bitcoind uses a guess for the value of verificationprogress. It will eventually get to // be 1, and we want to be less conservative. diff --git a/src/bitcoin/mod.rs b/src/bitcoin/mod.rs index 9c48ceb3..d54279b6 100644 --- a/src/bitcoin/mod.rs +++ b/src/bitcoin/mod.rs @@ -55,6 +55,9 @@ pub trait BitcoinInterface: Send { /// Get the best block info. fn chain_tip(&self) -> BlockChainTip; + /// Get the timestamp set in the best block's header. + fn tip_time(&self) -> u32; + /// Check whether this former tip is part of the current best chain. fn is_in_chain(&self, tip: &BlockChainTip) -> bool; @@ -88,6 +91,17 @@ pub trait BitcoinInterface: Send { /// Broadcast this transaction to the Bitcoin P2P network fn broadcast_tx(&self, tx: &bitcoin::Transaction) -> Result<(), BitcoinError>; + + /// Trigger a rescan of the block chain for transactions related to this descriptor since + /// the given date. + fn start_rescan(&self, desc: &descriptors::MultipathDescriptor, timestamp: u32); + + /// Rescan progress percentage. Between 0 and 1. + fn rescan_progress(&self) -> Option; + + /// Get the last block chain tip with a timestamp below this. Timestamp must be a valid block + /// timestamp. + fn block_before_date(&self, timestamp: u32) -> Option; } impl BitcoinInterface for d::BitcoinD { @@ -286,6 +300,24 @@ impl BitcoinInterface for d::BitcoinD { ), } } + + fn start_rescan(&self, desc: &descriptors::MultipathDescriptor, timestamp: u32) { + // FIXME: in theory i think this could potentially fail to actually start the rescan. + self.start_rescan(desc, timestamp); + } + + fn rescan_progress(&self) -> Option { + self.rescan_progress() + } + + fn block_before_date(&self, timestamp: u32) -> Option { + self.tip_before_timestamp(timestamp) + } + + fn tip_time(&self) -> u32 { + let tip = self.chain_tip(); + self.get_block_stats(tip.hash).time + } } // FIXME: do we need to repeat the entire trait implemenation? Isn't there a nicer way? @@ -342,6 +374,22 @@ impl BitcoinInterface for sync::Arc> fn broadcast_tx(&self, tx: &bitcoin::Transaction) -> Result<(), BitcoinError> { self.lock().unwrap().broadcast_tx(tx) } + + fn start_rescan(&self, desc: &descriptors::MultipathDescriptor, timestamp: u32) { + self.lock().unwrap().start_rescan(desc, timestamp) + } + + fn rescan_progress(&self) -> Option { + self.lock().unwrap().rescan_progress() + } + + fn block_before_date(&self, timestamp: u32) -> Option { + self.lock().unwrap().block_before_date(timestamp) + } + + fn tip_time(&self) -> u32 { + self.lock().unwrap().tip_time() + } } // FIXME: We could avoid this type (and all the conversions entailing allocations) if bitcoind diff --git a/src/testutils.rs b/src/testutils.rs index 6add2785..355e8bc3 100644 --- a/src/testutils.rs +++ b/src/testutils.rs @@ -74,6 +74,22 @@ impl BitcoinInterface for DummyBitcoind { fn broadcast_tx(&self, _: &bitcoin::Transaction) -> Result<(), BitcoinError> { todo!() } + + fn start_rescan(&self, _: &descriptors::MultipathDescriptor, _: u32) { + todo!() + } + + fn rescan_progress(&self) -> Option { + None + } + + fn block_before_date(&self, _: u32) -> Option { + todo!() + } + + fn tip_time(&self) -> u32 { + todo!() + } } pub struct DummyDb {