From 6997adc073981c3c786ab7e1244330fcc1d7afc7 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Tue, 26 Jul 2022 17:39:55 +0200 Subject: [PATCH] daemon: bitcoin: introduce the Bitcoin poller --- src/bitcoin/d/mod.rs | 23 ++++++++++++++++++ src/bitcoin/mod.rs | 16 ++++++++++++- src/bitcoin/poller/looper.rs | 46 ++++++++++++++++++++++++++++++++++++ src/bitcoin/poller/mod.rs | 31 ++++++++++++++++++++++++ src/lib.rs | 28 +++++++++++++++++++--- 5 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 src/bitcoin/poller/looper.rs create mode 100644 src/bitcoin/poller/mod.rs diff --git a/src/bitcoin/d/mod.rs b/src/bitcoin/d/mod.rs index a1e3d6ad..a5092bb9 100644 --- a/src/bitcoin/d/mod.rs +++ b/src/bitcoin/d/mod.rs @@ -477,4 +477,27 @@ impl BitcoinD { Ok(()) } + + pub fn sync_progress(&self) -> f64 { + // TODO: don't harass revaultd, be smarter like in revaultd. + roundup_progress( + self.make_node_request("getblockchaininfo", &[]) + .get("verificationprogress") + .and_then(Json::as_f64) + .expect("No valid 'verificationprogress' in getblockchaininfo response?"), + ) + } +} + +// Bitcoind uses a guess for the value of verificationprogress. It will eventually get to +// be 1, and we want to be less conservative. +fn roundup_progress(progress: f64) -> f64 { + let precision = 10u64.pow(5) as f64; + let progress_rounded = (progress * precision + 1.0) as u64; + + if progress_rounded * 10 >= precision as u64 { + 1.0 + } else { + (progress_rounded as f64 / precision) as f64 + } } diff --git a/src/bitcoin/mod.rs b/src/bitcoin/mod.rs index b3af0811..85b15724 100644 --- a/src/bitcoin/mod.rs +++ b/src/bitcoin/mod.rs @@ -2,5 +2,19 @@ ///! ///! Broadcast transactions, poll for new unspent coins, gather fee estimates. pub mod d; +pub mod poller; -pub trait BitcoinInterface {} +use std::sync; + +/// Our Bitcoin backend. +pub trait BitcoinInterface: Send { + /// Get the progress of the block chain synchronization. + /// Returns a percentage between 0 and 1. + fn sync_progress(&self) -> f64; +} + +impl BitcoinInterface for sync::Arc> { + fn sync_progress(&self) -> f64 { + self.read().unwrap().sync_progress() + } +} diff --git a/src/bitcoin/poller/looper.rs b/src/bitcoin/poller/looper.rs new file mode 100644 index 00000000..d0b284e7 --- /dev/null +++ b/src/bitcoin/poller/looper.rs @@ -0,0 +1,46 @@ +use crate::bitcoin::BitcoinInterface; + +use std::{ + sync::{self, atomic}, + thread, time, +}; + +/// Main event loop. Repeatedly polls the Bitcoin interface until told to stop through the +/// `shutdown` atomic. +pub fn looper( + bit: impl BitcoinInterface, + shutdown: sync::Arc, + poll_interval: time::Duration, +) { + let mut last_poll = None; + let mut synced = false; + + while !shutdown.load(atomic::Ordering::Relaxed) || last_poll.is_none() { + let now = time::Instant::now(); + + if let Some(last_poll) = last_poll { + if now.duration_since(last_poll) < poll_interval { + thread::sleep(time::Duration::from_millis(500)); + continue; + } + } + last_poll = Some(now); + + // Don't poll until the Bitcoin backend is fully synced. + if !synced { + let sync_progress = bit.sync_progress(); + log::info!( + "Block chain synchronization progress: {:.2}%", + sync_progress + ); + synced = sync_progress == 1.0; + if !synced { + // Avoid harassing bitcoind.. + // TODO: be smarter, like in revaultd, but more generic too. + #[cfg(not(test))] + thread::sleep(time::Duration::from_secs(30)); + continue; + } + } + } +} diff --git a/src/bitcoin/poller/mod.rs b/src/bitcoin/poller/mod.rs new file mode 100644 index 00000000..745b1757 --- /dev/null +++ b/src/bitcoin/poller/mod.rs @@ -0,0 +1,31 @@ +mod looper; + +use crate::bitcoin::{poller::looper::looper, BitcoinInterface}; + +use std::{ + sync::{self, atomic}, + thread, time, +}; + +/// The Bitcoin poller handler. +pub struct Poller { + handle: thread::JoinHandle<()>, + shutdown: sync::Arc, +} + +impl Poller { + pub fn start(bit: impl BitcoinInterface + 'static, poll_interval: time::Duration) -> Poller { + let shutdown = sync::Arc::from(atomic::AtomicBool::from(false)); + let handle = thread::spawn({ + let shutdown = shutdown.clone(); + move || looper(bit, shutdown, poll_interval) + }); + + Poller { shutdown, handle } + } + + pub fn stop(self) { + self.shutdown.store(true, atomic::Ordering::Relaxed); + self.handle.join().expect("The poller loop must not fail"); + } +} diff --git a/src/lib.rs b/src/lib.rs index 6a2fccb6..462c28d7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,12 +5,15 @@ mod daemonize; mod database; use crate::{ - bitcoin::d::{BitcoinD, BitcoindError}, + bitcoin::{ + d::{BitcoinD, BitcoindError}, + poller, + }, config::{config_folder_path, Config}, database::sqlite::{FreshDbOptions, SqliteDb, SqliteDbError}, }; -use std::{error, fmt, fs, io, path}; +use std::{error, fmt, fs, io, path, sync}; #[cfg(not(test))] use std::{panic, process}; @@ -173,7 +176,6 @@ impl DaemonHandle { } bitcoind.try_load_watchonly_wallet(); bitcoind.sanity_check(&config.main_descriptor, config.bitcoind_config.network)?; - bitcoind.with_retry_limit(None); log::info!("Connection to bitcoind established and checked."); // If we are on a UNIX system and they told us to daemonize, do it now. @@ -190,6 +192,12 @@ 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(bitcoind.clone(), config.bitcoind_config.poll_interval_secs); + bit_poller.stop(); + Ok(Self {}) } @@ -340,6 +348,18 @@ mod tests { stream.flush().unwrap(); } + // Send them a response to 'getblockchaininfo' saying we are far from being synced + fn complete_sync_check<'a>(server: &net::TcpListener) { + let net_resp = [ + "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"verificationprogress\":0.1}}\n".as_bytes(), + ] + .concat(); + let (mut stream, _) = server.accept().unwrap(); + read_til_json_end(&mut stream); + stream.write_all(&net_resp).unwrap(); + stream.flush().unwrap(); + } + #[test] fn daemon_startup() { let tmp_dir = env::temp_dir().join(format!( @@ -410,6 +430,7 @@ mod tests { complete_network_check(&server); complete_wallet_check(&server, &wo_path); complete_desc_check(&server, desc_str); + complete_sync_check(&server); daemon_thread.join().unwrap(); // The datadir is created now, so if we restart it it won't create the wo wallet. @@ -423,6 +444,7 @@ mod tests { complete_network_check(&server); complete_wallet_check(&server, &wo_path); complete_desc_check(&server, desc_str); + complete_sync_check(&server); daemon_thread.join().unwrap(); fs::remove_dir_all(&tmp_dir).unwrap();