diff --git a/src/bitcoin/mod.rs b/src/bitcoin/mod.rs index 25dcc522..4141edd9 100644 --- a/src/bitcoin/mod.rs +++ b/src/bitcoin/mod.rs @@ -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> = 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> ) -> 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 diff --git a/src/bitcoin/poller/looper.rs b/src/bitcoin/poller/looper.rs index 2cffcaa1..96bb7fd9 100644 --- a/src/bitcoin/poller/looper.rs +++ b/src/bitcoin/poller/looper.rs @@ -15,6 +15,7 @@ struct UpdatedCoins { pub received: Vec, 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); } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 5bdb6278..b7fd2782 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -197,7 +197,7 @@ impl DaemonControl { pub fn list_coins(&self) -> ListCoinsResult { let mut db_conn = self.db.connection(); let coins: Vec = db_conn - .unspent_coins() + .list_unspent_coins() // Can't use into_values as of Rust 1.48 .into_iter() .map(|(_, coin)| { diff --git a/src/database/mod.rs b/src/database/mod.rs index ff2fafa1..f0b261bc 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -55,7 +55,10 @@ pub trait DatabaseConnection { ) -> Option; /// Get all UTxOs. - fn unspent_coins(&mut self) -> HashMap; + fn list_unspent_coins(&mut self) -> HashMap; + + /// List coins that are being spent and whose spending transaction is still unconfirmed. + fn list_spending_coins(&mut self) -> HashMap; /// 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 { - db_coins_into_coins(self.unspent_coins()) + fn list_unspent_coins(&mut self) -> HashMap { + self.list_unspent_coins() + .into_iter() + .map(|db_coin| (db_coin.outpoint, db_coin.into())) + .collect() + } + + fn list_spending_coins(&mut self) -> HashMap { + 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 { - 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 { @@ -170,37 +186,6 @@ impl DatabaseConnection for SqliteConn { } } -// FIXME: if possible, avoid reallocating. -fn db_coins_into_coins(db_coins: Vec) -> HashMap { - 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, } +impl std::convert::From 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(&self, h: &mut H) { self.outpoint.hash(h) diff --git a/src/database/sqlite/mod.rs b/src/database/sqlite/mod.rs index a4e672e3..58404e73 100644 --- a/src/database/sqlite/mod.rs +++ b/src/database/sqlite/mod.rs @@ -253,7 +253,7 @@ impl SqliteConn { } /// Get all UTxOs. - pub fn unspent_coins(&mut self) -> Vec { + pub fn list_unspent_coins(&mut self) -> Vec { 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 { + 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, + ) { + 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 { 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 = 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 = 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 = 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 = 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); diff --git a/src/testutils.rs b/src/testutils.rs index 1ef694c7..d3462407 100644 --- a/src/testutils.rs +++ b/src/testutils.rs @@ -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 { + fn list_unspent_coins(&mut self) -> HashMap { self.db.read().unwrap().coins.clone() } + fn list_spending_coins(&mut self) -> HashMap { + 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