bitcoin: interface for coins discovery and updates
This commit is contained in:
parent
05b3af1b5a
commit
c6a25adfcd
@ -3,7 +3,7 @@
|
|||||||
///! We use the RPC interface and a watchonly descriptor wallet.
|
///! We use the RPC interface and a watchonly descriptor wallet.
|
||||||
use crate::{bitcoin::BlockChainTip, config};
|
use crate::{bitcoin::BlockChainTip, config};
|
||||||
|
|
||||||
use std::{convert::TryInto, fs, io, str::FromStr, time::Duration};
|
use std::{collections::HashSet, convert::TryInto, fs, io, str::FromStr, time::Duration};
|
||||||
|
|
||||||
use jsonrpc::{
|
use jsonrpc::{
|
||||||
arg,
|
arg,
|
||||||
@ -278,6 +278,14 @@ impl BitcoinD {
|
|||||||
.expect("We must not fail to make a request for more than a minute")
|
.expect("We must not fail to make a request for more than a minute")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn make_faillible_wallet_request(
|
||||||
|
&self,
|
||||||
|
method: &str,
|
||||||
|
params: &[Box<serde_json::value::RawValue>],
|
||||||
|
) -> Result<Json, BitcoindError> {
|
||||||
|
self.make_request(&self.watchonly_client, method, params)
|
||||||
|
}
|
||||||
|
|
||||||
fn get_bitcoind_version(&self) -> u64 {
|
fn get_bitcoind_version(&self) -> u64 {
|
||||||
self.make_node_request("getnetworkinfo", &[])
|
self.make_node_request("getnetworkinfo", &[])
|
||||||
.get("version")
|
.get("version")
|
||||||
@ -521,8 +529,134 @@ impl BitcoinD {
|
|||||||
.expect("bitcoind must send valid block hashes"),
|
.expect("bitcoind must send valid block hashes"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
pub fn list_since_block(&self, block_hash: &bitcoin::BlockHash) -> LSBlockRes {
|
||||||
|
self.make_wallet_request(
|
||||||
|
"listsinceblock",
|
||||||
|
¶ms!(Json::String(block_hash.to_string()),),
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_transaction(&self, txid: &bitcoin::Txid) -> Option<GetTxRes> {
|
||||||
|
// TODO: Maybe assert we got a -5 error, and not any other kind of error?
|
||||||
|
self.make_faillible_wallet_request(
|
||||||
|
"gettransaction",
|
||||||
|
¶ms!(Json::String(txid.to_string())),
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
.map(|res| res.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Efficient check that a coin is spent.
|
||||||
|
pub fn is_spent(&self, op: &bitcoin::OutPoint) -> bool {
|
||||||
|
// The result of gettxout is empty if the outpoint is spent.
|
||||||
|
self.make_node_request(
|
||||||
|
"gettxout",
|
||||||
|
¶ms!(
|
||||||
|
Json::String(op.txid.to_string()),
|
||||||
|
Json::Number(op.vout.into())
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.get("bestblock")
|
||||||
|
.is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// So, bitcoind has no API for getting the transaction spending a wallet UTXO. Instead we are
|
||||||
|
/// therefore using a rather convoluted way to get it the other way around, since the spending
|
||||||
|
/// transaction is actually *part of the wallet transactions*.
|
||||||
|
/// So, what we do there is listing all outgoing transactions of the wallet since the last poll
|
||||||
|
/// and iterating through each of those to check if it spends the transaction we are interested
|
||||||
|
/// in (requiring an other RPC call for each!!).
|
||||||
|
pub fn get_spender_txid(&self, spent_outpoint: &bitcoin::OutPoint) -> Option<bitcoin::Txid> {
|
||||||
|
// Get the hash of the block parent of the spent transaction's block.
|
||||||
|
let req = self.make_wallet_request(
|
||||||
|
"gettransaction",
|
||||||
|
¶ms!(Json::String(spent_outpoint.txid.to_string())),
|
||||||
|
);
|
||||||
|
let spent_tx_height = match req.get("blockheight").and_then(Json::as_i64) {
|
||||||
|
Some(h) => h,
|
||||||
|
// FIXME: we assume it's confirmed. If we were to change the logic in the poller, we'd
|
||||||
|
// need to handle it here.
|
||||||
|
None => return None,
|
||||||
|
};
|
||||||
|
let block_hash = if let Ok(res) = self.make_fallible_node_request(
|
||||||
|
"getblockhash",
|
||||||
|
¶ms!(Json::Number((spent_tx_height - 1).into())),
|
||||||
|
) {
|
||||||
|
res.as_str()
|
||||||
|
.expect("'getblockhash' result isn't a string")
|
||||||
|
.to_string()
|
||||||
|
} else {
|
||||||
|
// Possibly a race.
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Now we can get all transactions related to us since the spent transaction confirmed.
|
||||||
|
// We'll use it to locate the spender.
|
||||||
|
let lsb_res =
|
||||||
|
self.make_wallet_request("listsinceblock", ¶ms!(Json::String(block_hash)));
|
||||||
|
let transactions = lsb_res
|
||||||
|
.get("transactions")
|
||||||
|
.and_then(Json::as_array)
|
||||||
|
.expect("tx array must be there");
|
||||||
|
|
||||||
|
// Get the spent txid to ignore the entries about this transaction
|
||||||
|
let spent_txid = spent_outpoint.txid.to_string();
|
||||||
|
// We use a cache to avoid needless iterations, since listsinceblock returns an entry
|
||||||
|
// per transaction output, not per transaction.
|
||||||
|
let mut visited_txs = HashSet::with_capacity(transactions.len());
|
||||||
|
for transaction in transactions {
|
||||||
|
if transaction.get("category").and_then(Json::as_str) != Some("send") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let spending_txid = transaction
|
||||||
|
.get("txid")
|
||||||
|
.and_then(Json::as_str)
|
||||||
|
.expect("A valid txid must be present");
|
||||||
|
if visited_txs.contains(&spending_txid) || &spent_txid == spending_txid {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
visited_txs.insert(spending_txid);
|
||||||
|
}
|
||||||
|
|
||||||
|
let gettx_res = self.make_wallet_request(
|
||||||
|
"gettransaction",
|
||||||
|
¶ms!(
|
||||||
|
Json::String(spending_txid.to_string()),
|
||||||
|
Json::Bool(true), // watchonly
|
||||||
|
Json::Bool(true) // verbose
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let vin = gettx_res
|
||||||
|
.get("decoded")
|
||||||
|
.and_then(|d| d.get("vin").and_then(Json::as_array))
|
||||||
|
.expect("A valid vin array must be present");
|
||||||
|
|
||||||
|
for input in vin {
|
||||||
|
let txid = input
|
||||||
|
.get("txid")
|
||||||
|
.and_then(Json::as_str)
|
||||||
|
.and_then(|t| bitcoin::Txid::from_str(t).ok())
|
||||||
|
.expect("A valid txid must be present");
|
||||||
|
let vout = input
|
||||||
|
.get("vout")
|
||||||
|
.and_then(Json::as_u64)
|
||||||
|
.expect("A valid vout must be present") as u32;
|
||||||
|
let input_outpoint = bitcoin::OutPoint { txid, vout };
|
||||||
|
|
||||||
|
if spent_outpoint == &input_outpoint {
|
||||||
|
return bitcoin::Txid::from_str(spending_txid)
|
||||||
|
.map(Some)
|
||||||
|
.expect("Must be a valid txid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
// Bitcoind uses a guess for the value of verificationprogress. It will eventually get to
|
// Bitcoind uses a guess for the value of verificationprogress. It will eventually get to
|
||||||
// be 1, and we want to be less conservative.
|
// be 1, and we want to be less conservative.
|
||||||
fn roundup_progress(progress: f64) -> f64 {
|
fn roundup_progress(progress: f64) -> f64 {
|
||||||
@ -535,3 +669,88 @@ fn roundup_progress(progress: f64) -> f64 {
|
|||||||
(progress_rounded as f64 / precision) as f64
|
(progress_rounded as f64 / precision) as f64
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A 'received' entry in the 'listsinceblock' result.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct LSBlockEntry {
|
||||||
|
pub outpoint: bitcoin::OutPoint,
|
||||||
|
pub amount: bitcoin::Amount,
|
||||||
|
pub block_height: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Json> for LSBlockEntry {
|
||||||
|
fn from(json: &Json) -> LSBlockEntry {
|
||||||
|
let txid = json
|
||||||
|
.get("txid")
|
||||||
|
.and_then(Json::as_str)
|
||||||
|
.and_then(|s| bitcoin::Txid::from_str(s).ok())
|
||||||
|
.expect("bitcoind can't give a bad block hash");
|
||||||
|
let vout = json
|
||||||
|
.get("vout")
|
||||||
|
.and_then(Json::as_u64)
|
||||||
|
.expect("bitcoind can't give a bad vout") as u32;
|
||||||
|
let outpoint = bitcoin::OutPoint { txid, vout };
|
||||||
|
|
||||||
|
// Must be a received entry, hence not negative.
|
||||||
|
let amount = json
|
||||||
|
.get("amount")
|
||||||
|
.and_then(Json::as_f64)
|
||||||
|
.and_then(|a| bitcoin::Amount::from_btc(a).ok())
|
||||||
|
.expect("bitcoind won't give us a bad amount");
|
||||||
|
let block_height = json
|
||||||
|
.get("blockheight")
|
||||||
|
.and_then(Json::as_i64)
|
||||||
|
.map(|bh| bh as i32);
|
||||||
|
|
||||||
|
LSBlockEntry {
|
||||||
|
outpoint,
|
||||||
|
amount,
|
||||||
|
block_height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct LSBlockRes {
|
||||||
|
pub received_coins: Vec<LSBlockEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Json> for LSBlockRes {
|
||||||
|
fn from(json: Json) -> LSBlockRes {
|
||||||
|
let received_coins = json
|
||||||
|
.get("transactions")
|
||||||
|
.and_then(Json::as_array)
|
||||||
|
.expect("Array must be present")
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|j| {
|
||||||
|
if j.get("category")
|
||||||
|
.and_then(Json::as_str)
|
||||||
|
.expect("must be present")
|
||||||
|
== "receive"
|
||||||
|
{
|
||||||
|
let lsb_entry: LSBlockEntry = j.into();
|
||||||
|
Some(lsb_entry)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
LSBlockRes { received_coins }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct GetTxRes {
|
||||||
|
pub block_height: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Json> for GetTxRes {
|
||||||
|
fn from(json: Json) -> GetTxRes {
|
||||||
|
let block_height = json
|
||||||
|
.get("blockheight")
|
||||||
|
.and_then(Json::as_i64)
|
||||||
|
.map(|bh| bh as i32);
|
||||||
|
GetTxRes { block_height }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -4,9 +4,11 @@
|
|||||||
pub mod d;
|
pub mod d;
|
||||||
pub mod poller;
|
pub mod poller;
|
||||||
|
|
||||||
|
use d::LSBlockEntry;
|
||||||
|
|
||||||
use std::sync;
|
use std::sync;
|
||||||
|
|
||||||
use miniscript::bitcoin;
|
use miniscript::bitcoin::{self, hashes::Hash};
|
||||||
|
|
||||||
/// Information about the best block in the chain
|
/// Information about the best block in the chain
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Copy)]
|
#[derive(Debug, Clone, Eq, PartialEq, Copy)]
|
||||||
@ -17,6 +19,8 @@ pub struct BlockChainTip {
|
|||||||
|
|
||||||
/// Our Bitcoin backend.
|
/// Our Bitcoin backend.
|
||||||
pub trait BitcoinInterface: Send {
|
pub trait BitcoinInterface: Send {
|
||||||
|
fn genesis_block(&self) -> BlockChainTip;
|
||||||
|
|
||||||
/// Get the progress of the block chain synchronization.
|
/// Get the progress of the block chain synchronization.
|
||||||
/// Returns a percentage between 0 and 1.
|
/// Returns a percentage between 0 and 1.
|
||||||
fn sync_progress(&self) -> f64;
|
fn sync_progress(&self) -> f64;
|
||||||
@ -26,9 +30,29 @@ pub trait BitcoinInterface: Send {
|
|||||||
|
|
||||||
/// Check whether this former tip is part of the current best chain.
|
/// Check whether this former tip is part of the current best chain.
|
||||||
fn is_in_chain(&self, tip: &BlockChainTip) -> bool;
|
fn is_in_chain(&self, tip: &BlockChainTip) -> bool;
|
||||||
|
|
||||||
|
/// Get coins received since the specified tip.
|
||||||
|
fn received_coins(&self, tip: &BlockChainTip) -> Vec<UTxO>;
|
||||||
|
|
||||||
|
/// Get all coins that were confirmed, and at what height.
|
||||||
|
fn confirmed_coins(&self, outpoints: &[bitcoin::OutPoint]) -> Vec<(bitcoin::OutPoint, i32)>;
|
||||||
|
|
||||||
|
/// Get all coins that were spent, and the spending txid.
|
||||||
|
fn spent_coins(
|
||||||
|
&self,
|
||||||
|
outpoints: &[bitcoin::OutPoint],
|
||||||
|
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid)>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BitcoinInterface for d::BitcoinD {
|
impl BitcoinInterface for d::BitcoinD {
|
||||||
|
fn genesis_block(&self) -> BlockChainTip {
|
||||||
|
let height = 0;
|
||||||
|
let hash = self
|
||||||
|
.get_block_hash(height)
|
||||||
|
.expect("Genesis block hash must always be there");
|
||||||
|
BlockChainTip { hash, height }
|
||||||
|
}
|
||||||
|
|
||||||
fn sync_progress(&self) -> f64 {
|
fn sync_progress(&self) -> f64 {
|
||||||
self.sync_progress()
|
self.sync_progress()
|
||||||
}
|
}
|
||||||
@ -42,10 +66,78 @@ impl BitcoinInterface for d::BitcoinD {
|
|||||||
.map(|bh| bh == tip.hash)
|
.map(|bh| bh == tip.hash)
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn received_coins(&self, tip: &BlockChainTip) -> 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| {
|
||||||
|
let LSBlockEntry {
|
||||||
|
outpoint,
|
||||||
|
amount,
|
||||||
|
block_height,
|
||||||
|
} = entry;
|
||||||
|
UTxO {
|
||||||
|
outpoint,
|
||||||
|
amount,
|
||||||
|
block_height,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirmed_coins(&self, outpoints: &[bitcoin::OutPoint]) -> Vec<(bitcoin::OutPoint, i32)> {
|
||||||
|
let mut confirmed = Vec::with_capacity(outpoints.len());
|
||||||
|
|
||||||
|
for op in outpoints {
|
||||||
|
// TODO: batch those calls to gettransaction
|
||||||
|
if let Some(res) = self.get_transaction(&op.txid) {
|
||||||
|
if let Some(h) = res.block_height {
|
||||||
|
confirmed.push((*op, h));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::error!("Transaction not in wallet for coin '{}'.", op);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmed
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spent_coins(
|
||||||
|
&self,
|
||||||
|
outpoints: &[bitcoin::OutPoint],
|
||||||
|
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid)> {
|
||||||
|
let mut spent = Vec::with_capacity(outpoints.len());
|
||||||
|
|
||||||
|
for op in outpoints {
|
||||||
|
if self.is_spent(&op) {
|
||||||
|
let spending_txid = if let Some(txid) = self.get_spender_txid(&op) {
|
||||||
|
txid
|
||||||
|
} else {
|
||||||
|
// TODO: better handling of this edge case.
|
||||||
|
log::error!(
|
||||||
|
"Could not get spender of '{}'. Using a dummy spending txid.",
|
||||||
|
op
|
||||||
|
);
|
||||||
|
bitcoin::Txid::from_slice(&[0; 32][..]).unwrap()
|
||||||
|
};
|
||||||
|
spent.push((*op, spending_txid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spent
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: do we need to repeat the entire trait implemenation? Isn't there a nicer way?
|
// FIXME: do we need to repeat the entire trait implemenation? Isn't there a nicer way?
|
||||||
impl BitcoinInterface for sync::Arc<sync::Mutex<dyn BitcoinInterface + 'static>> {
|
impl BitcoinInterface for sync::Arc<sync::Mutex<dyn BitcoinInterface + 'static>> {
|
||||||
|
fn genesis_block(&self) -> BlockChainTip {
|
||||||
|
self.lock().unwrap().genesis_block()
|
||||||
|
}
|
||||||
|
|
||||||
fn sync_progress(&self) -> f64 {
|
fn sync_progress(&self) -> f64 {
|
||||||
self.lock().unwrap().sync_progress()
|
self.lock().unwrap().sync_progress()
|
||||||
}
|
}
|
||||||
@ -57,4 +149,28 @@ impl BitcoinInterface for sync::Arc<sync::Mutex<dyn BitcoinInterface + 'static>>
|
|||||||
fn is_in_chain(&self, tip: &BlockChainTip) -> bool {
|
fn is_in_chain(&self, tip: &BlockChainTip) -> bool {
|
||||||
self.lock().unwrap().is_in_chain(tip)
|
self.lock().unwrap().is_in_chain(tip)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn received_coins(&self, tip: &BlockChainTip) -> Vec<UTxO> {
|
||||||
|
self.lock().unwrap().received_coins(tip)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirmed_coins(&self, outpoints: &[bitcoin::OutPoint]) -> Vec<(bitcoin::OutPoint, i32)> {
|
||||||
|
self.lock().unwrap().confirmed_coins(outpoints)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spent_coins(
|
||||||
|
&self,
|
||||||
|
outpoints: &[bitcoin::OutPoint],
|
||||||
|
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid)> {
|
||||||
|
self.lock().unwrap().spent_coins(outpoints)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: We could avoid this type (and all the conversions entailing allocations) if bitcoind
|
||||||
|
// exposed the derivation index from the parent descriptor in the LSB result.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct UTxO {
|
||||||
|
pub outpoint: bitcoin::OutPoint,
|
||||||
|
pub amount: bitcoin::Amount,
|
||||||
|
pub block_height: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
bitcoin::{BitcoinInterface, BlockChainTip},
|
bitcoin::{BitcoinInterface, BlockChainTip, UTxO},
|
||||||
config::{BitcoinConfig, Config},
|
config::{BitcoinConfig, Config},
|
||||||
database::{Coin, DatabaseConnection, DatabaseInterface},
|
database::{Coin, DatabaseConnection, DatabaseInterface},
|
||||||
DaemonHandle,
|
DaemonHandle,
|
||||||
@ -15,6 +15,14 @@ use miniscript::{
|
|||||||
pub struct DummyBitcoind {}
|
pub struct DummyBitcoind {}
|
||||||
|
|
||||||
impl BitcoinInterface for DummyBitcoind {
|
impl BitcoinInterface for DummyBitcoind {
|
||||||
|
fn genesis_block(&self) -> BlockChainTip {
|
||||||
|
let hash = bitcoin::BlockHash::from_str(
|
||||||
|
"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
BlockChainTip { hash, height: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
fn sync_progress(&self) -> f64 {
|
fn sync_progress(&self) -> f64 {
|
||||||
1.0
|
1.0
|
||||||
}
|
}
|
||||||
@ -32,6 +40,18 @@ impl BitcoinInterface for DummyBitcoind {
|
|||||||
// No reorg
|
// No reorg
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn received_coins(&self, _: &BlockChainTip) -> Vec<UTxO> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirmed_coins(&self, _: &[bitcoin::OutPoint]) -> Vec<(bitcoin::OutPoint, i32)> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spent_coins(&self, _: &[bitcoin::OutPoint]) -> Vec<(bitcoin::OutPoint, bitcoin::Txid)> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DummyDb {
|
pub struct DummyDb {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user