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.
This commit is contained in:
Antoine Poinsot 2022-10-21 15:37:16 +02:00
parent ba4c1e0383
commit 7a18c583cb
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304
7 changed files with 78 additions and 23 deletions

View File

@ -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<i32>,
pub address: bitcoin::Address,
pub parent_descs: Vec<descriptor::Descriptor<descriptor::DescriptorPublicKey>>,
}
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::<Option<Vec<_>>>()
})
.expect("bitcoind can't give invalid descriptors");
LSBlockEntry {
outpoint,
amount,
block_height,
address,
parent_descs,
}
}
}

View File

@ -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<UTxO>;
fn received_coins(
&self,
tip: &BlockChainTip,
desc: &descriptors::InheritanceDescriptor,
) -> Vec<UTxO>;
/// 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<UTxO> {
fn received_coins(
&self,
tip: &BlockChainTip,
desc: &descriptors::InheritanceDescriptor,
) -> Vec<UTxO> {
// 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<sync::Mutex<dyn BitcoinInterface + 'static>>
self.lock().unwrap().is_in_chain(tip)
}
fn received_coins(&self, tip: &BlockChainTip) -> Vec<UTxO> {
self.lock().unwrap().received_coins(tip)
fn received_coins(
&self,
tip: &BlockChainTip,
desc: &descriptors::InheritanceDescriptor,
) -> Vec<UTxO> {
self.lock().unwrap().received_coins(tip, desc)
}
fn confirmed_coins(

View File

@ -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<dyn DatabaseConnection>,
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, &current_tip);
let updated_coins = update_coins(bit, &mut db_conn, &current_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<sync::Mutex<dyn DatabaseInterface>>,
shutdown: sync::Arc<atomic::AtomicBool>,
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);
}
}

View File

@ -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<sync::Mutex<dyn BitcoinInterface>>,
db: sync::Arc<sync::Mutex<dyn DatabaseInterface>>,
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");

View File

@ -263,6 +263,12 @@ impl str::FromStr for InheritanceDescriptor {
}
}
impl PartialEq<descriptor::Descriptor<descriptor::DescriptorPublicKey>> for InheritanceDescriptor {
fn eq(&self, other: &descriptor::Descriptor<descriptor::DescriptorPublicKey>) -> bool {
self.0.eq(other)
}
}
impl InheritanceDescriptor {
pub fn new(
owner_key: descriptor::DescriptorPublicKey,

View File

@ -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.

View File

@ -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<UTxO> {
fn received_coins(
&self,
_: &BlockChainTip,
_: &descriptors::InheritanceDescriptor,
) -> Vec<UTxO> {
Vec::new()
}