bitcoin: update our tip in the poller

This introduces the DB connection in the poller thread
This commit is contained in:
Antoine Poinsot 2022-07-27 11:27:21 +02:00
parent 6997adc073
commit dd37255d7b
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304
7 changed files with 230 additions and 23 deletions

View File

@ -1,9 +1,9 @@
///! Implementation of the Bitcoin interface using bitcoind.
///!
///! We use the RPC interface and a watchonly descriptor wallet.
use crate::config;
use crate::{bitcoin::BlockChainTip, config};
use std::{fs, io, time::Duration};
use std::{convert::TryInto, fs, io, str::FromStr, time::Duration};
use jsonrpc::{
arg,
@ -478,15 +478,49 @@ impl BitcoinD {
Ok(())
}
fn block_chain_info(&self) -> Json {
self.make_node_request("getblockchaininfo", &[])
}
pub fn sync_progress(&self) -> f64 {
// TODO: don't harass revaultd, be smarter like in revaultd.
roundup_progress(
self.make_node_request("getblockchaininfo", &[])
self.block_chain_info()
.get("verificationprogress")
.and_then(Json::as_f64)
.expect("No valid 'verificationprogress' in getblockchaininfo response?"),
)
}
pub fn chain_tip(&self) -> BlockChainTip {
// We use getblockchaininfo to avoid a race between getblockcount and getblockhash
let chain_info = self.block_chain_info();
let hash = bitcoin::BlockHash::from_str(
chain_info
.get("bestblockhash")
.and_then(Json::as_str)
.expect("No valid 'bestblockhash' in 'getblockchaininfo' response?"),
)
.expect("Invalid blockhash from bitcoind?");
let height: i32 = chain_info
.get("blocks")
.and_then(Json::as_i64)
.expect("No valid 'blocks' in 'getblockchaininfo' response?")
.try_into()
.expect("Must fit by Bitcoin consensus");
BlockChainTip { hash, height }
}
pub fn get_block_hash(&self, height: i32) -> Option<bitcoin::BlockHash> {
Some(
self.make_fallible_node_request("getblockhash", &params!(Json::Number(height.into()),))
.ok()?
.as_str()
.and_then(|s| bitcoin::BlockHash::from_str(s).ok())
.expect("bitcoind must send valid block hashes"),
)
}
}
// Bitcoind uses a guess for the value of verificationprogress. It will eventually get to
@ -495,7 +529,7 @@ 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 {
if progress_rounded >= precision as u64 {
1.0
} else {
(progress_rounded as f64 / precision) as f64

View File

@ -6,15 +6,42 @@ pub mod poller;
use std::sync;
use miniscript::bitcoin;
/// Information about the best block in the chain
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct BlockChainTip {
pub hash: bitcoin::BlockHash,
pub height: i32,
}
/// 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;
/// Get the best block info.
fn chain_tip(&self) -> BlockChainTip;
/// Check whether this former tip is part of the current best chain.
fn is_in_chain(&self, tip: &BlockChainTip) -> bool;
}
impl BitcoinInterface for sync::Arc<sync::RwLock<d::BitcoinD>> {
fn sync_progress(&self) -> f64 {
self.read().unwrap().sync_progress()
}
fn chain_tip(&self) -> BlockChainTip {
self.read().unwrap().chain_tip()
}
fn is_in_chain(&self, tip: &BlockChainTip) -> bool {
self.read()
.unwrap()
.get_block_hash(tip.height)
.map(|bh| bh == tip.hash)
.unwrap_or(false)
}
}

View File

@ -1,14 +1,46 @@
use crate::bitcoin::BitcoinInterface;
use crate::{
bitcoin::BitcoinInterface,
database::{DatabaseConnection, DatabaseInterface},
};
use std::{
sync::{self, atomic},
thread, time,
};
fn update_tip(bit: &impl BitcoinInterface, db_conn: &mut Box<dyn DatabaseConnection>) {
let bitcoin_tip = bit.chain_tip();
let current_tip = match db_conn.chain_tip() {
Some(tip) => tip,
None => {
db_conn.update_tip(&bitcoin_tip);
return;
}
};
// If the tip didn't change, there is nothing to update.
if current_tip == bitcoin_tip {
return;
}
if bitcoin_tip.height > current_tip.height {
// Make sure we are on the same chain.
if bit.is_in_chain(&current_tip) {
// All good, we just moved forward. Record the new tip.
db_conn.update_tip(&bitcoin_tip);
return;
}
}
// TODO: reorg handling.
}
/// Main event loop. Repeatedly polls the Bitcoin interface until told to stop through the
/// `shutdown` atomic.
pub fn looper(
bit: impl BitcoinInterface,
db: impl DatabaseInterface,
shutdown: sync::Arc<atomic::AtomicBool>,
poll_interval: time::Duration,
) {
@ -42,5 +74,8 @@ pub fn looper(
continue;
}
}
let mut db_conn = db.connection();
update_tip(&bit, &mut db_conn);
}
}

View File

@ -1,6 +1,9 @@
mod looper;
use crate::bitcoin::{poller::looper::looper, BitcoinInterface};
use crate::{
bitcoin::{poller::looper::looper, BitcoinInterface},
database::DatabaseInterface,
};
use std::{
sync::{self, atomic},
@ -14,11 +17,15 @@ pub struct Poller {
}
impl Poller {
pub fn start(bit: impl BitcoinInterface + 'static, poll_interval: time::Duration) -> Poller {
pub fn start(
bit: impl BitcoinInterface + 'static,
db: impl DatabaseInterface + '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)
move || looper(bit, db, shutdown, poll_interval)
});
Poller { shutdown, handle }

View File

@ -3,4 +3,42 @@
///! Record wallet metadata, spent and unspent coins, ongoing transactions.
pub mod sqlite;
pub trait DatabaseInterface {}
use crate::{
bitcoin::BlockChainTip,
database::sqlite::{schema::DbTip, SqliteConn, SqliteDb},
};
pub trait DatabaseInterface: Send {
fn connection(&self) -> Box<dyn DatabaseConnection>;
}
impl DatabaseInterface for SqliteDb {
fn connection(&self) -> Box<dyn DatabaseConnection> {
Box::new(self.connection().expect("Database must be available"))
}
}
pub trait DatabaseConnection {
/// Get the tip of the best chain we've seen.
fn chain_tip(&mut self) -> Option<BlockChainTip>;
/// Update our best chain seen.
fn update_tip(&mut self, tip: &BlockChainTip);
}
impl DatabaseConnection for SqliteConn {
fn chain_tip(&mut self) -> Option<BlockChainTip> {
match self.db_tip() {
DbTip {
block_height: Some(height),
block_hash: Some(hash),
..
} => Some(BlockChainTip { height, hash }),
_ => None,
}
}
fn update_tip(&mut self, tip: &BlockChainTip) {
self.update_tip(&tip)
}
}

View File

@ -6,11 +6,16 @@
///!
///! We leverage SQLite's `unlock_notify` feature to synchronize writes accross connection. More
///! about it at https://sqlite.org/unlock_notify.html.
mod schema;
pub mod schema;
mod utils;
use schema::{DbTip, DbWallet};
use utils::{create_fresh_db, db_query};
use crate::{
bitcoin::BlockChainTip,
database::sqlite::{
schema::{DbTip, DbWallet},
utils::{create_fresh_db, db_exec, db_query},
},
};
use std::{convert::TryInto, fmt, io, path};
@ -178,6 +183,19 @@ impl SqliteConn {
.pop()
.expect("There is always a row in the wallet table")
}
/// Update the network tip.
pub fn update_tip(&mut self, tip: &BlockChainTip) {
db_exec(&mut self.conn, |db_tx| {
db_tx
.execute(
"UPDATE tip SET blockheight = (?1), blockhash = (?2)",
rusqlite::params![tip.height, tip.hash.to_vec()],
)
.map(|_| ())
})
.expect("Database must be available")
}
}
#[cfg(test)]
@ -185,6 +203,15 @@ mod tests {
use super::*;
use std::{env, fs, path, process, str::FromStr, thread};
fn dummy_options() -> FreshDbOptions {
let desc_str = "wsh(andor(pk(03b506a1dbe57b4bf48c95e0c7d417b87dd3b4349d290d2e7e9ba72c912652d80a),older(10000),pk(0295e7f5d12a2061f1fd2286cefec592dff656a19f55f4f01305d6aa56630880ce)))";
let main_descriptor = Descriptor::<DescriptorPublicKey>::from_str(desc_str).unwrap();
FreshDbOptions {
bitcoind_network: bitcoin::Network::Bitcoin,
main_descriptor,
}
}
#[test]
fn db_startup_sanity_checks() {
let tmp_dir = env::temp_dir().join(format!(
@ -202,15 +229,10 @@ mod tests {
.to_string()
.contains("database file not found"));
let desc_str = "wsh(andor(pk(03b506a1dbe57b4bf48c95e0c7d417b87dd3b4349d290d2e7e9ba72c912652d80a),older(10000),pk(0295e7f5d12a2061f1fd2286cefec592dff656a19f55f4f01305d6aa56630880ce)))";
let desc = Descriptor::<DescriptorPublicKey>::from_str(desc_str).unwrap();
let options = FreshDbOptions {
bitcoind_network: bitcoin::Network::Bitcoin,
main_descriptor: desc.clone(),
};
let options = dummy_options();
let db = SqliteDb::new(db_path.clone(), Some(options.clone())).unwrap();
db.sanity_check(bitcoin::Network::Testnet, &desc)
db.sanity_check(bitcoin::Network::Testnet, &options.main_descriptor)
.unwrap_err()
.to_string()
.contains("Database was created for network");
@ -226,9 +248,50 @@ mod tests {
// TODO: version check
let db = SqliteDb::new(db_path.clone(), Some(options.clone())).unwrap();
db.sanity_check(bitcoin::Network::Bitcoin, &desc).unwrap();
db.sanity_check(bitcoin::Network::Bitcoin, &options.main_descriptor)
.unwrap();
let db = SqliteDb::new(db_path.clone(), None).unwrap();
db.sanity_check(bitcoin::Network::Bitcoin, &desc).unwrap();
db.sanity_check(bitcoin::Network::Bitcoin, &options.main_descriptor)
.unwrap();
fs::remove_dir_all(&tmp_dir).unwrap();
}
#[test]
fn db_tip_update() {
let tmp_dir = env::temp_dir().join(format!(
"minisafed-unit-tests-{}-{:?}",
process::id(),
thread::current().id()
));
fs::create_dir_all(&tmp_dir).unwrap();
let db_path: path::PathBuf = [tmp_dir.as_path(), path::Path::new("minisafed.sqlite3")]
.iter()
.collect();
let options = dummy_options();
let db = SqliteDb::new(db_path.clone(), Some(options.clone())).unwrap();
{
let mut conn = db.connection().unwrap();
let db_tip = conn.db_tip();
assert!(
db_tip.block_hash.is_none()
&& db_tip.block_height.is_none()
&& db_tip.network == options.bitcoind_network
);
let new_tip = BlockChainTip {
height: 746756,
hash: bitcoin::BlockHash::from_str(
"00000000000000000006d50e4c9fd269ddf690c94f422dff85e96f1a84b3a615",
)
.unwrap(),
};
conn.update_tip(&new_tip);
let db_tip = conn.db_tip();
assert_eq!(db_tip.block_height.unwrap(), new_tip.height);
assert_eq!(db_tip.block_hash.unwrap(), new_tip.hash);
}
fs::remove_dir_all(&tmp_dir).unwrap();
}

View File

@ -194,8 +194,11 @@ 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);
let bit_poller = poller::Poller::start(
bitcoind.clone(),
db,
config.bitcoind_config.poll_interval_secs,
);
bit_poller.stop();
Ok(Self {})