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.
|
||||
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::{
|
||||
arg,
|
||||
@ -278,6 +278,14 @@ impl BitcoinD {
|
||||
.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 {
|
||||
self.make_node_request("getnetworkinfo", &[])
|
||||
.get("version")
|
||||
@ -521,8 +529,134 @@ impl BitcoinD {
|
||||
.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
|
||||
// be 1, and we want to be less conservative.
|
||||
fn roundup_progress(progress: f64) -> f64 {
|
||||
@ -535,3 +669,88 @@ fn roundup_progress(progress: f64) -> 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 poller;
|
||||
|
||||
use d::LSBlockEntry;
|
||||
|
||||
use std::sync;
|
||||
|
||||
use miniscript::bitcoin;
|
||||
use miniscript::bitcoin::{self, hashes::Hash};
|
||||
|
||||
/// Information about the best block in the chain
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Copy)]
|
||||
@ -17,6 +19,8 @@ pub struct BlockChainTip {
|
||||
|
||||
/// Our Bitcoin backend.
|
||||
pub trait BitcoinInterface: Send {
|
||||
fn genesis_block(&self) -> BlockChainTip;
|
||||
|
||||
/// Get the progress of the block chain synchronization.
|
||||
/// Returns a percentage between 0 and 1.
|
||||
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.
|
||||
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 {
|
||||
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 {
|
||||
self.sync_progress()
|
||||
}
|
||||
@ -42,10 +66,78 @@ impl BitcoinInterface for d::BitcoinD {
|
||||
.map(|bh| bh == tip.hash)
|
||||
.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?
|
||||
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 {
|
||||
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 {
|
||||
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::{
|
||||
bitcoin::{BitcoinInterface, BlockChainTip},
|
||||
bitcoin::{BitcoinInterface, BlockChainTip, UTxO},
|
||||
config::{BitcoinConfig, Config},
|
||||
database::{Coin, DatabaseConnection, DatabaseInterface},
|
||||
DaemonHandle,
|
||||
@ -15,6 +15,14 @@ use miniscript::{
|
||||
pub struct 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 {
|
||||
1.0
|
||||
}
|
||||
@ -32,6 +40,18 @@ impl BitcoinInterface for DummyBitcoind {
|
||||
// No reorg
|
||||
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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user