add spent_coins to bitcoind poller

This commit is contained in:
edouard 2022-09-16 17:26:14 +02:00
parent 94ee94edbd
commit 3cf6bcbb98
6 changed files with 186 additions and 45 deletions

View File

@ -6,6 +6,7 @@ pub mod poller;
use d::LSBlockEntry;
use std::collections::HashMap;
use std::sync;
use miniscript::bitcoin::{self, hashes::Hash};
@ -45,6 +46,12 @@ pub trait BitcoinInterface: Send {
&self,
outpoints: &[bitcoin::OutPoint],
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid)>;
/// Get all coins that are spent with the final spend tx txid and blocktime.
fn spent_coins(
&self,
outpoints: &[(bitcoin::OutPoint, bitcoin::Txid)],
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid, u32)>;
}
impl BitcoinInterface for d::BitcoinD {
@ -141,6 +148,39 @@ impl BitcoinInterface for d::BitcoinD {
spent
}
fn spent_coins(
&self,
outpoints: &[(bitcoin::OutPoint, bitcoin::Txid)],
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid, u32)> {
let mut spent = Vec::with_capacity(outpoints.len());
let mut cache: HashMap<bitcoin::Txid, Option<d::GetTxRes>> = HashMap::new();
for (op, txid) in outpoints {
let tx: Option<&d::GetTxRes> = match cache.get(txid) {
Some(tx) => tx.as_ref(),
None => {
let tx = self.get_transaction(txid);
cache.insert(*txid, tx);
cache.get(txid).unwrap().as_ref()
}
};
if let Some(tx) = tx {
if let Some(block_height) = tx.block_height {
if block_height > 1 {
spent.push((*op, *txid, tx.block_time.expect("Spend is confirmed")))
}
} else {
// TODO: handle the case where new transaction which txid is not the
// coin spending_txid, spent the coin.
}
}
}
spent
}
}
// FIXME: do we need to repeat the entire trait implemenation? Isn't there a nicer way?
@ -178,6 +218,13 @@ impl BitcoinInterface for sync::Arc<sync::Mutex<dyn BitcoinInterface + 'static>>
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid)> {
self.lock().unwrap().spending_coins(outpoints)
}
fn spent_coins(
&self,
outpoints: &[(bitcoin::OutPoint, bitcoin::Txid)],
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid, u32)> {
self.lock().unwrap().spent_coins(outpoints)
}
}
// FIXME: We could avoid this type (and all the conversions entailing allocations) if bitcoind

View File

@ -15,6 +15,7 @@ struct UpdatedCoins {
pub received: Vec<Coin>,
pub confirmed: Vec<(bitcoin::OutPoint, i32, u32)>,
pub spending: Vec<(bitcoin::OutPoint, bitcoin::Txid)>,
pub spent: Vec<(bitcoin::OutPoint, bitcoin::Txid, u32)>,
}
// Update the state of our coins. There may be new unspent, and existing ones may become confirmed
@ -27,7 +28,7 @@ fn update_coins(
previous_tip: &BlockChainTip,
) -> UpdatedCoins {
// Start by fetching newly received coins.
let curr_coins = db_conn.unspent_coins();
let curr_coins = db_conn.list_unspent_coins();
let mut received = Vec::new();
for utxo in bit.received_coins(&previous_tip) {
if let Some(derivation_index) = db_conn.derivation_index_by_address(&utxo.address) {
@ -87,10 +88,21 @@ fn update_coins(
.collect();
let spending = bit.spending_coins(&to_be_spent);
// We need to confirm coins that are currently spending and which transactions are now in a
// block.
let mut spending_coins: Vec<(bitcoin::OutPoint, bitcoin::Txid)> = db_conn
.list_spending_coins()
.values()
.map(|coin| (coin.outpoint, coin.spend_txid.expect("Coin is spending")))
.collect();
spending_coins.extend(&spending);
let spent = bit.spent_coins(spending_coins.as_slice());
UpdatedCoins {
received,
confirmed,
spending,
spent,
}
}
@ -139,6 +151,7 @@ fn updates(bit: &impl BitcoinInterface, db: &impl DatabaseInterface) {
db_conn.new_unspent_coins(&updated_coins.received);
db_conn.confirm_coins(&updated_coins.confirmed);
db_conn.spend_coins(&updated_coins.spending);
db_conn.confirm_spend(&updated_coins.spent);
if let Some(tip) = new_tip {
db_conn.update_tip(&tip);
}

View File

@ -197,7 +197,7 @@ impl DaemonControl {
pub fn list_coins(&self) -> ListCoinsResult {
let mut db_conn = self.db.connection();
let coins: Vec<ListCoinsEntry> = db_conn
.unspent_coins()
.list_unspent_coins()
// Can't use into_values as of Rust 1.48
.into_iter()
.map(|(_, coin)| {

View File

@ -55,7 +55,10 @@ pub trait DatabaseConnection {
) -> Option<bip32::ChildNumber>;
/// Get all UTxOs.
fn unspent_coins(&mut self) -> HashMap<bitcoin::OutPoint, Coin>;
fn list_unspent_coins(&mut self) -> HashMap<bitcoin::OutPoint, Coin>;
/// List coins that are being spent and whose spending transaction is still unconfirmed.
fn list_spending_coins(&mut self) -> HashMap<bitcoin::OutPoint, Coin>;
/// Store new UTxOs. Coins must not already be in database.
fn new_unspent_coins<'a>(&mut self, coins: &[Coin]);
@ -63,7 +66,7 @@ pub trait DatabaseConnection {
/// Mark a set of coins as being confirmed at a specified height and block time.
fn confirm_coins<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, i32, u32)]);
/// Mark a set of coins as being spent by a specified txid.
/// Mark a set of coins as being spent by a specified txid of a pending transaction.
fn spend_coins<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid)]);
/// Mark a set of coins as spent by a specified txid at a specified block time.
@ -115,8 +118,18 @@ impl DatabaseConnection for SqliteConn {
self.increment_derivation_index(secp)
}
fn unspent_coins(&mut self) -> HashMap<bitcoin::OutPoint, Coin> {
db_coins_into_coins(self.unspent_coins())
fn list_unspent_coins(&mut self) -> HashMap<bitcoin::OutPoint, Coin> {
self.list_unspent_coins()
.into_iter()
.map(|db_coin| (db_coin.outpoint, db_coin.into()))
.collect()
}
fn list_spending_coins(&mut self) -> HashMap<bitcoin::OutPoint, Coin> {
self.list_spending_coins()
.into_iter()
.map(|db_coin| (db_coin.outpoint, db_coin.into()))
.collect()
}
fn new_unspent_coins<'a>(&mut self, coins: &[Coin]) {
@ -147,7 +160,10 @@ impl DatabaseConnection for SqliteConn {
&mut self,
outpoints: &[bitcoin::OutPoint],
) -> HashMap<bitcoin::OutPoint, Coin> {
db_coins_into_coins(self.db_coins(outpoints))
self.db_coins(outpoints)
.into_iter()
.map(|db_coin| (db_coin.outpoint, db_coin.into()))
.collect()
}
fn spend_tx(&mut self, txid: &bitcoin::Txid) -> Option<Psbt> {
@ -170,37 +186,6 @@ impl DatabaseConnection for SqliteConn {
}
}
// FIXME: if possible, avoid reallocating.
fn db_coins_into_coins(db_coins: Vec<DbCoin>) -> HashMap<bitcoin::OutPoint, Coin> {
db_coins
.into_iter()
.map(|db_coin| {
let DbCoin {
outpoint,
block_height,
block_time,
amount,
derivation_index,
spend_txid,
spent_at,
..
} = db_coin;
(
outpoint,
Coin {
outpoint,
block_height,
block_time,
amount,
derivation_index,
spend_txid,
spent_at,
},
)
})
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Coin {
pub outpoint: bitcoin::OutPoint,
@ -212,6 +197,30 @@ pub struct Coin {
pub spent_at: Option<u32>,
}
impl std::convert::From<DbCoin> for Coin {
fn from(db_coin: DbCoin) -> Coin {
let DbCoin {
outpoint,
block_height,
block_time,
amount,
derivation_index,
spend_txid,
spent_at,
..
} = db_coin;
Coin {
outpoint,
block_height,
block_time,
amount,
derivation_index,
spend_txid,
spent_at,
}
}
}
impl std::hash::Hash for Coin {
fn hash<H: std::hash::Hasher>(&self, h: &mut H) {
self.outpoint.hash(h)

View File

@ -253,7 +253,7 @@ impl SqliteConn {
}
/// Get all UTxOs.
pub fn unspent_coins(&mut self) -> Vec<DbCoin> {
pub fn list_unspent_coins(&mut self) -> Vec<DbCoin> {
db_query(
&mut self.conn,
"SELECT * FROM coins WHERE spend_txid is NULL",
@ -263,6 +263,17 @@ impl SqliteConn {
.expect("Db must not fail")
}
/// List coins that are being spent and whose spending transaction is still unconfirmed.
pub fn list_spending_coins(&mut self) -> Vec<DbCoin> {
db_query(
&mut self.conn,
"SELECT * FROM coins WHERE spend_txid IS NOT NULL AND spent_at IS NULL",
rusqlite::params![],
|row| row.try_into(),
)
.expect("Db must not fail")
}
// FIXME: don't take the whole coin, we don't need it.
/// Store new, unconfirmed and unspent, coins.
/// Will panic if given a coin that is already in DB.
@ -323,6 +334,29 @@ impl SqliteConn {
.expect("Database must be available")
}
/// Mark a set of coins as spent.
pub fn confirm_spend<'a>(
&mut self,
outpoints: impl IntoIterator<Item = &'a (bitcoin::OutPoint, bitcoin::Txid, u32)>,
) {
db_exec(&mut self.conn, |db_tx| {
for (outpoint, spend_txid, time) in outpoints {
db_tx.execute(
"UPDATE coins SET spend_txid = ?1, spent_at = ?2 WHERE txid = ?3 AND vout = ?4",
rusqlite::params![
spend_txid.to_vec(),
time,
outpoint.txid.to_vec(),
outpoint.vout,
],
)?;
}
Ok(())
})
.expect("Database must be available")
}
pub fn db_address(&mut self, address: &bitcoin::Address) -> Option<DbAddress> {
db_query(
&mut self.conn,
@ -519,7 +553,7 @@ mod tests {
let mut conn = db.connection().unwrap();
// Necessarily empty at first.
assert!(conn.unspent_coins().is_empty());
assert!(conn.list_unspent_coins().is_empty());
// Add one, we'll get it.
let coin_a = Coin {
@ -535,7 +569,7 @@ mod tests {
spent_at: None,
};
conn.new_unspent_coins(&[coin_a.clone()]); // On 1.48, arrays aren't IntoIterator
assert_eq!(conn.unspent_coins()[0].outpoint, coin_a.outpoint);
assert_eq!(conn.list_unspent_coins()[0].outpoint, coin_a.outpoint);
// We can query it by its outpoint
let coins = conn.db_coins(&[coin_a.outpoint]);
@ -557,7 +591,7 @@ mod tests {
};
conn.new_unspent_coins(&[coin_b.clone()]);
let outpoints: HashSet<bitcoin::OutPoint> = conn
.unspent_coins()
.list_unspent_coins()
.into_iter()
.map(|c| c.outpoint)
.collect();
@ -586,7 +620,7 @@ mod tests {
let height = 174500;
let time = 174500;
conn.confirm_coins(&[(coin_a.outpoint, height, time)]);
let coins = conn.unspent_coins();
let coins = conn.list_unspent_coins();
assert_eq!(coins[0].block_height, Some(height));
assert_eq!(coins[0].block_time, Some(time));
assert!(coins[1].block_height.is_none());
@ -598,13 +632,34 @@ mod tests {
bitcoin::Txid::from_slice(&[0; 32][..]).unwrap(),
)]);
let outpoints: HashSet<bitcoin::OutPoint> = conn
.unspent_coins()
.list_unspent_coins()
.into_iter()
.map(|c| c.outpoint)
.collect();
assert!(!outpoints.contains(&coin_a.outpoint));
assert!(outpoints.contains(&coin_b.outpoint));
let outpoints: HashSet<bitcoin::OutPoint> = conn
.list_spending_coins()
.into_iter()
.map(|c| c.outpoint)
.collect();
assert!(outpoints.contains(&coin_a.outpoint));
// Now if we confirm the spend.
conn.confirm_spend(&[(
coin_a.outpoint,
bitcoin::Txid::from_slice(&[0; 32][..]).unwrap(),
3,
)]);
// the coin is not in a spending state.
let outpoints: HashSet<bitcoin::OutPoint> = conn
.list_spending_coins()
.into_iter()
.map(|c| c.outpoint)
.collect();
assert!(outpoints.is_empty());
// Both are still in DB
let coins = conn.db_coins(&[coin_a.outpoint, coin_b.outpoint]);
assert_eq!(coins.len(), 2);

View File

@ -55,6 +55,13 @@ impl BitcoinInterface for DummyBitcoind {
fn spending_coins(&self, _: &[bitcoin::OutPoint]) -> Vec<(bitcoin::OutPoint, bitcoin::Txid)> {
Vec::new()
}
fn spent_coins(
&self,
_: &[(bitcoin::OutPoint, bitcoin::Txid)],
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid, u32)> {
Vec::new()
}
}
pub struct DummyDb {
@ -107,10 +114,20 @@ impl DatabaseConnection for DummyDbConn {
self.db.write().unwrap().curr_index = next_index;
}
fn unspent_coins(&mut self) -> HashMap<bitcoin::OutPoint, Coin> {
fn list_unspent_coins(&mut self) -> HashMap<bitcoin::OutPoint, Coin> {
self.db.read().unwrap().coins.clone()
}
fn list_spending_coins(&mut self) -> HashMap<bitcoin::OutPoint, Coin> {
let mut result = HashMap::new();
for (k, v) in self.db.read().unwrap().coins.iter() {
if !v.spend_txid.is_none() {
result.insert(k.clone(), v.clone());
}
}
result
}
fn new_unspent_coins<'a>(&mut self, coins: &[Coin]) {
for coin in coins {
self.db