From 7a18c583cbfcd958db9c79e8b13a0a68c1d20d41 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Fri, 21 Oct 2022 15:37:16 +0200 Subject: [PATCH] bitcoind: filter received coins based on parent descriptors Our bitcoind watchonly wallet could, maybe, have other descriptors that were imported. Sounds pretty unlikely since we use a dedicated wallet but hey. More importantly, we'll need to know the parent descriptor of the coin in order to recognize it as newly received or change. --- src/bitcoin/d/mod.rs | 21 +++++++++++++++--- src/bitcoin/mod.rs | 42 ++++++++++++++++++++++++++---------- src/bitcoin/poller/looper.rs | 19 ++++++++++------ src/bitcoin/poller/mod.rs | 4 +++- src/descriptors.rs | 6 ++++++ src/lib.rs | 1 + src/testutils.rs | 8 +++++-- 7 files changed, 78 insertions(+), 23 deletions(-) diff --git a/src/bitcoin/d/mod.rs b/src/bitcoin/d/mod.rs index 4de7a4bb..4c010c1d 100644 --- a/src/bitcoin/d/mod.rs +++ b/src/bitcoin/d/mod.rs @@ -10,7 +10,7 @@ use jsonrpc::{ client::Client, simple_http::{self, SimpleHttpTransport}, }; -use miniscript::bitcoin; +use miniscript::{bitcoin, descriptor}; use serde_json::Value as Json; @@ -532,8 +532,8 @@ impl BitcoinD { Json::String(block_hash.to_string()), Json::Number(1.into()), // Default for min_confirmations for the returned Json::Bool(true), // Whether to include watchonly - Json::Bool(false), // Whether to include an array of txs that were removed in reorgs - Json::Bool(true) // Whether to include UTxOs treated as change. + Json::Bool(false), // Whether to include an array of txs that were removed in reorgs + Json::Bool(true) // Whether to include UTxOs treated as change. ), ) .into() @@ -714,6 +714,7 @@ pub struct LSBlockEntry { pub amount: bitcoin::Amount, pub block_height: Option, pub address: bitcoin::Address, + pub parent_descs: Vec>, } impl From<&Json> for LSBlockEntry { @@ -745,12 +746,26 @@ impl From<&Json> for LSBlockEntry { .and_then(Json::as_str) .and_then(|s| bitcoin::Address::from_str(s).ok()) .expect("bitcoind can't give a bad address"); + let parent_descs = json + .get("parent_descs") + .and_then(Json::as_array) + .and_then(|descs| { + descs + .iter() + .map(|desc| { + desc.as_str() + .and_then(|s| descriptor::Descriptor::<_>::from_str(s).ok()) + }) + .collect::>>() + }) + .expect("bitcoind can't give invalid descriptors"); LSBlockEntry { outpoint, amount, block_height, address, + parent_descs, } } } diff --git a/src/bitcoin/mod.rs b/src/bitcoin/mod.rs index ec7cf141..a95de2d8 100644 --- a/src/bitcoin/mod.rs +++ b/src/bitcoin/mod.rs @@ -4,7 +4,10 @@ pub mod d; pub mod poller; -use d::{BitcoindError, LSBlockEntry}; +use crate::{ + bitcoin::d::{BitcoindError, LSBlockEntry}, + descriptors, +}; use std::{collections::HashMap, error, fmt, sync}; @@ -56,7 +59,11 @@ pub trait BitcoinInterface: Send { fn is_in_chain(&self, tip: &BlockChainTip) -> bool; /// Get coins received since the specified tip. - fn received_coins(&self, tip: &BlockChainTip) -> Vec; + fn received_coins( + &self, + tip: &BlockChainTip, + desc: &descriptors::InheritanceDescriptor, + ) -> Vec; /// Get all coins that were confirmed, and at what height and time. fn confirmed_coins( @@ -106,25 +113,34 @@ impl BitcoinInterface for d::BitcoinD { .unwrap_or(false) } - fn received_coins(&self, tip: &BlockChainTip) -> Vec { + fn received_coins( + &self, + tip: &BlockChainTip, + desc: &descriptors::InheritanceDescriptor, + ) -> Vec { // TODO: don't assume only a single descriptor is loaded on the wo wallet let lsb_res = self.list_since_block(&tip.hash); lsb_res .received_coins .into_iter() - .map(|entry| { + .filter_map(|entry| { let LSBlockEntry { outpoint, amount, block_height, address, + parent_descs, } = entry; - UTxO { - outpoint, - amount, - block_height, - address, + if parent_descs.iter().any(|parent_desc| desc == parent_desc) { + Some(UTxO { + outpoint, + amount, + block_height, + address, + }) + } else { + None } }) .collect() @@ -287,8 +303,12 @@ impl BitcoinInterface for sync::Arc> self.lock().unwrap().is_in_chain(tip) } - fn received_coins(&self, tip: &BlockChainTip) -> Vec { - self.lock().unwrap().received_coins(tip) + fn received_coins( + &self, + tip: &BlockChainTip, + desc: &descriptors::InheritanceDescriptor, + ) -> Vec { + self.lock().unwrap().received_coins(tip, desc) } fn confirmed_coins( diff --git a/src/bitcoin/poller/looper.rs b/src/bitcoin/poller/looper.rs index 25aa39ae..77751092 100644 --- a/src/bitcoin/poller/looper.rs +++ b/src/bitcoin/poller/looper.rs @@ -1,6 +1,7 @@ use crate::{ bitcoin::{BitcoinInterface, BlockChainTip, UTxO}, database::{Coin, DatabaseConnection, DatabaseInterface}, + descriptors, }; use std::{ @@ -26,13 +27,14 @@ fn update_coins( bit: &impl BitcoinInterface, db_conn: &mut Box, previous_tip: &BlockChainTip, + desc: &descriptors::InheritanceDescriptor, ) -> UpdatedCoins { let curr_coins = db_conn.coins(); log::debug!("Current coins: {:?}", curr_coins); // Start by fetching newly received coins. let mut received = Vec::new(); - for utxo in bit.received_coins(previous_tip) { + for utxo in bit.received_coins(previous_tip, desc) { if let Some(derivation_index) = db_conn.derivation_index_by_address(&utxo.address) { if !curr_coins.contains_key(&utxo.outpoint) { let UTxO { @@ -154,7 +156,11 @@ fn new_tip(bit: &impl BitcoinInterface, current_tip: &BlockChainTip) -> TipUpdat TipUpdate::Reorged(common_ancestor) } -fn updates(bit: &impl BitcoinInterface, db: &impl DatabaseInterface) { +fn updates( + bit: &impl BitcoinInterface, + db: &impl DatabaseInterface, + desc: &descriptors::InheritanceDescriptor, +) { let mut db_conn = db.connection(); // Check if there was a new block before updating ourselves. @@ -167,18 +173,18 @@ fn updates(bit: &impl BitcoinInterface, db: &impl DatabaseInterface) { // between our former chain and the new one, then restart fresh. db_conn.rollback_tip(&new_tip); log::info!("Tip was rolled back to '{}'.", new_tip); - return updates(bit, db); + return updates(bit, db, desc); } }; // Then check the state of our coins. Do it even if the tip did not change since last poll, as // we may have unconfirmed transactions. - let updated_coins = update_coins(bit, &mut db_conn, ¤t_tip); + let updated_coins = update_coins(bit, &mut db_conn, ¤t_tip, desc); // If the tip changed while we were polling our Bitcoin interface, start over. if bit.chain_tip() != latest_tip { log::info!("Chain tip changed while we were updating our state. Starting over."); - return updates(bit, db); + return updates(bit, db, desc); } // The chain tip did not change since we started our updates. Record them and the latest tip. @@ -213,6 +219,7 @@ pub fn looper( db: sync::Arc>, shutdown: sync::Arc, poll_interval: time::Duration, + desc: descriptors::InheritanceDescriptor, ) { let mut last_poll = None; let mut synced = false; @@ -247,6 +254,6 @@ pub fn looper( } } - updates(&bit, &db); + updates(&bit, &db, &desc); } } diff --git a/src/bitcoin/poller/mod.rs b/src/bitcoin/poller/mod.rs index 97a1c22d..7fe3e860 100644 --- a/src/bitcoin/poller/mod.rs +++ b/src/bitcoin/poller/mod.rs @@ -3,6 +3,7 @@ mod looper; use crate::{ bitcoin::{poller::looper::looper, BitcoinInterface}, database::DatabaseInterface, + descriptors, }; use std::{ @@ -21,13 +22,14 @@ impl Poller { bit: sync::Arc>, db: sync::Arc>, poll_interval: time::Duration, + desc: descriptors::InheritanceDescriptor, ) -> Poller { let shutdown = sync::Arc::from(atomic::AtomicBool::from(false)); let handle = thread::Builder::new() .name("Bitcoin poller".to_string()) .spawn({ let shutdown = shutdown.clone(); - move || looper(bit, db, shutdown, poll_interval) + move || looper(bit, db, shutdown, poll_interval, desc) }) .expect("Must not fail"); diff --git a/src/descriptors.rs b/src/descriptors.rs index 05974816..46b44009 100644 --- a/src/descriptors.rs +++ b/src/descriptors.rs @@ -263,6 +263,12 @@ impl str::FromStr for InheritanceDescriptor { } } +impl PartialEq> for InheritanceDescriptor { + fn eq(&self, other: &descriptor::Descriptor) -> bool { + self.0.eq(other) + } +} + impl InheritanceDescriptor { pub fn new( owner_key: descriptor::DescriptorPublicKey, diff --git a/src/lib.rs b/src/lib.rs index 702a9c87..a8a6cfe2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -317,6 +317,7 @@ impl DaemonHandle { bit.clone(), db.clone(), config.bitcoin_config.poll_interval_secs, + config.main_descriptor.clone(), ); // Finally, set up the API. diff --git a/src/testutils.rs b/src/testutils.rs index f738d503..7b11ab9a 100644 --- a/src/testutils.rs +++ b/src/testutils.rs @@ -2,7 +2,7 @@ use crate::{ bitcoin::{BitcoinError, BitcoinInterface, BlockChainTip, UTxO}, config::{BitcoinConfig, Config}, database::{Coin, DatabaseConnection, DatabaseInterface, SpendBlock}, - DaemonHandle, + descriptors, DaemonHandle, }; use std::{collections::HashMap, env, fs, io, path, process, str::FromStr, sync, thread, time}; @@ -44,7 +44,11 @@ impl BitcoinInterface for DummyBitcoind { true } - fn received_coins(&self, _: &BlockChainTip) -> Vec { + fn received_coins( + &self, + _: &BlockChainTip, + _: &descriptors::InheritanceDescriptor, + ) -> Vec { Vec::new() }