diff --git a/src/bitcoin/mod.rs b/src/bitcoin/mod.rs index e9c2e6ba..0682af9e 100644 --- a/src/bitcoin/mod.rs +++ b/src/bitcoin/mod.rs @@ -51,7 +51,7 @@ pub trait BitcoinInterface: Send { fn spent_coins( &self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid)], - ) -> Vec<(bitcoin::OutPoint, bitcoin::Txid, u32)>; + ) -> Vec<(bitcoin::OutPoint, bitcoin::Txid, i32, u32)>; /// Get the common ancestor between the Bitcoin backend's tip and the given tip. fn common_ancestor(&self, tip: &BlockChainTip) -> BlockChainTip; @@ -155,7 +155,7 @@ impl BitcoinInterface for d::BitcoinD { fn spent_coins( &self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid)], - ) -> Vec<(bitcoin::OutPoint, bitcoin::Txid, u32)> { + ) -> Vec<(bitcoin::OutPoint, bitcoin::Txid, i32, u32)> { let mut spent = Vec::with_capacity(outpoints.len()); let mut cache: HashMap> = HashMap::new(); @@ -174,10 +174,15 @@ impl BitcoinInterface for d::BitcoinD { let mut txs_to_cache: Vec<(bitcoin::Txid, Option)> = Vec::new(); if let Some(tx) = tx { - if let Some(block_time) = tx.block_time { + if let Some(block_height) = tx.block_height { // TODO: make both block time and height under the same Option. assert!(tx.block_height.is_some()); - spent.push((*op, *txid, block_time)) + spent.push(( + *op, + *txid, + block_height, + tx.block_time.expect("Confirmed tx."), + )); } else if !tx.conflicting_txs.is_empty() { for txid in &tx.conflicting_txs { let tx: Option<&d::GetTxRes> = match cache.get(txid) { @@ -190,13 +195,12 @@ impl BitcoinInterface for d::BitcoinD { }; 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"), - )) - } + spent.push(( + *op, + *txid, + block_height, + tx.block_time.expect("Spend is confirmed"), + )) } } } @@ -266,7 +270,7 @@ impl BitcoinInterface for sync::Arc> fn spent_coins( &self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid)], - ) -> Vec<(bitcoin::OutPoint, bitcoin::Txid, u32)> { + ) -> Vec<(bitcoin::OutPoint, bitcoin::Txid, i32, u32)> { self.lock().unwrap().spent_coins(outpoints) } diff --git a/src/bitcoin/poller/looper.rs b/src/bitcoin/poller/looper.rs index 7eff2c14..a10557e8 100644 --- a/src/bitcoin/poller/looper.rs +++ b/src/bitcoin/poller/looper.rs @@ -15,7 +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)>, + pub spent: Vec<(bitcoin::OutPoint, bitcoin::Txid, i32, u32)>, } // Update the state of our coins. There may be new unspent, and existing ones may become confirmed @@ -43,7 +43,7 @@ fn update_coins( block_height: None, block_time: None, spend_txid: None, - spend_block_time: None, + spend_block: None, }; received.push(coin); } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index e8d0a67a..4afcaffc 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -572,7 +572,7 @@ mod tests { amount: bitcoin::Amount::from_sat(100_000), derivation_index: bip32::ChildNumber::from(13), spend_txid: None, - spend_block_time: None, + spend_block: None, }]); let res = control.create_spend(&[dummy_op], &destinations, 1).unwrap(); let tx = res.psbt.global.unsigned_tx; @@ -666,7 +666,7 @@ mod tests { amount: bitcoin::Amount::from_sat(100_000), derivation_index: bip32::ChildNumber::from(13), spend_txid: None, - spend_block_time: None, + spend_block: None, }, Coin { outpoint: dummy_op_b, @@ -675,7 +675,7 @@ mod tests { amount: bitcoin::Amount::from_sat(115_680), derivation_index: bip32::ChildNumber::from(34), spend_txid: None, - spend_block_time: None, + spend_block: None, }, ]); diff --git a/src/database/mod.rs b/src/database/mod.rs index 64518eb4..b356c9ed 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -6,7 +6,7 @@ pub mod sqlite; use crate::{ bitcoin::BlockChainTip, database::sqlite::{ - schema::{DbCoin, DbTip}, + schema::{DbCoin, DbSpendBlock, DbTip}, SqliteConn, SqliteDb, }, }; @@ -70,7 +70,7 @@ pub trait DatabaseConnection { fn spend_coins(&mut self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid)]); /// Mark a set of coins as spent by a specified txid at a specified block time. - fn confirm_spend(&mut self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid, u32)]); + fn confirm_spend(&mut self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid, i32, u32)]); /// Get specific coins from the database. fn coins_by_outpoints( @@ -144,7 +144,7 @@ impl DatabaseConnection for SqliteConn { self.spend_coins(outpoints) } - fn confirm_spend<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid, u32)]) { + fn confirm_spend<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid, i32, u32)]) { self.confirm_spend(outpoints) } @@ -186,6 +186,21 @@ impl DatabaseConnection for SqliteConn { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct SpendBlock { + pub height: i32, + pub time: u32, +} + +impl From for SpendBlock { + fn from(b: DbSpendBlock) -> SpendBlock { + SpendBlock { + height: b.height, + time: b.time, + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Coin { pub outpoint: bitcoin::OutPoint, @@ -194,7 +209,7 @@ pub struct Coin { pub amount: bitcoin::Amount, pub derivation_index: bip32::ChildNumber, pub spend_txid: Option, - pub spend_block_time: Option, + pub spend_block: Option, } impl std::convert::From for Coin { @@ -206,7 +221,7 @@ impl std::convert::From for Coin { amount, derivation_index, spend_txid, - spend_block_time, + spend_block, .. } = db_coin; Coin { @@ -216,7 +231,7 @@ impl std::convert::From for Coin { amount, derivation_index, spend_txid, - spend_block_time, + spend_block: spend_block.map(SpendBlock::from), } } } diff --git a/src/database/sqlite/mod.rs b/src/database/sqlite/mod.rs index ab321036..b9f410b9 100644 --- a/src/database/sqlite/mod.rs +++ b/src/database/sqlite/mod.rs @@ -334,17 +334,19 @@ impl SqliteConn { .expect("Database must be available") } - /// Mark a set of coins as spent. + /// Mark the Spend transaction of a given set of coins as being confirmed at a given + /// block. pub fn confirm_spend<'a>( &mut self, - outpoints: impl IntoIterator, + outpoints: impl IntoIterator, ) { db_exec(&mut self.conn, |db_tx| { - for (outpoint, spend_txid, time) in outpoints { + for (outpoint, spend_txid, height, time) in outpoints { db_tx.execute( - "UPDATE coins SET spend_txid = ?1, spend_block_time = ?2 WHERE txid = ?3 AND vout = ?4", + "UPDATE coins SET spend_txid = ?1, spend_block_height = ?2, spend_block_time = ?3 WHERE txid = ?4 AND vout = ?5", rusqlite::params![ spend_txid.to_vec(), + height, time, outpoint.txid.to_vec(), outpoint.vout, @@ -566,7 +568,7 @@ mod tests { amount: bitcoin::Amount::from_sat(98765), derivation_index: bip32::ChildNumber::from_normal_idx(10).unwrap(), spend_txid: None, - spend_block_time: None, + spend_block: 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); @@ -587,7 +589,7 @@ mod tests { amount: bitcoin::Amount::from_sat(1111), derivation_index: bip32::ChildNumber::from_normal_idx(103).unwrap(), spend_txid: None, - spend_block_time: None, + spend_block: None, }; conn.new_unspent_coins(&[coin_b.clone()]); let outpoints: HashSet = conn @@ -641,10 +643,13 @@ mod tests { assert!(outpoints.contains(&coin_a.outpoint)); // Now if we confirm the spend. + let height = 128_097; + let time = 3_000_000; conn.confirm_spend(&[( coin_a.outpoint, bitcoin::Txid::from_slice(&[0; 32][..]).unwrap(), - 3, + height, + time, )]); // the coin is not in a spending state. let outpoints: HashSet = conn @@ -657,6 +662,12 @@ mod tests { // Both are still in DB let coins = conn.db_coins(&[coin_a.outpoint, coin_b.outpoint]); assert_eq!(coins.len(), 2); + + // The confirmed one contains the right time and block height + let coin = conn.db_coins(&[coin_a.outpoint]).pop().unwrap(); + assert!(coin.spend_block.is_some()); + assert_eq!(coin.spend_block.as_ref().unwrap().time, time); + assert_eq!(coin.spend_block.unwrap().height, height); } fs::remove_dir_all(&tmp_dir).unwrap(); diff --git a/src/database/sqlite/schema.rs b/src/database/sqlite/schema.rs index 1719d3cc..ed71476e 100644 --- a/src/database/sqlite/schema.rs +++ b/src/database/sqlite/schema.rs @@ -30,7 +30,11 @@ CREATE TABLE wallets ( deposit_derivation_index INTEGER NOT NULL ); -/* Our (U)TxOs. */ +/* Our (U)TxOs. + * + * The 'spend_block_height' and 'spend_block.time' are only present if the spending + * transaction for this coin exists and was confirmed. + */ CREATE TABLE coins ( id INTEGER PRIMARY KEY NOT NULL, wallet_id INTEGER NOT NULL, @@ -41,7 +45,7 @@ CREATE TABLE coins ( amount_sat INTEGER NOT NULL, derivation_index INTEGER NOT NULL, spend_txid BLOB, - /* Time of the block containing the transaction spending the coin, NULL if not confirmed */ + spend_block_height INTEGER, spend_block_time INTEGER, UNIQUE (txid, vout), FOREIGN KEY (wallet_id) REFERENCES wallets (id) @@ -126,6 +130,12 @@ impl TryFrom<&rusqlite::Row<'_>> for DbWallet { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct DbSpendBlock { + pub height: i32, + pub time: u32, +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct DbCoin { pub id: i64, @@ -136,7 +146,7 @@ pub struct DbCoin { pub amount: bitcoin::Amount, pub derivation_index: bip32::ChildNumber, pub spend_txid: Option, - pub spend_block_time: Option, + pub spend_block: Option, } impl TryFrom<&rusqlite::Row<'_>> for DbCoin { @@ -161,7 +171,13 @@ impl TryFrom<&rusqlite::Row<'_>> for DbCoin { let spend_txid: Option> = row.get(8)?; let spend_txid = spend_txid.map(|txid| encode::deserialize(&txid).expect("We only store valid txids")); - let spend_block_time = row.get(9)?; + let spend_height: Option = row.get(9)?; + let spend_time: Option = row.get(10)?; + assert_eq!(spend_height.is_none(), spend_time.is_none()); + let spend_block = spend_height.map(|height| DbSpendBlock { + height, + time: spend_time.expect("Must be there if height is"), + }); Ok(DbCoin { id, @@ -172,7 +188,7 @@ impl TryFrom<&rusqlite::Row<'_>> for DbCoin { amount, derivation_index, spend_txid, - spend_block_time, + spend_block, }) } } diff --git a/src/testutils.rs b/src/testutils.rs index d660bd55..cdb5e9ce 100644 --- a/src/testutils.rs +++ b/src/testutils.rs @@ -1,7 +1,7 @@ use crate::{ bitcoin::{BitcoinInterface, BlockChainTip, UTxO}, config::{BitcoinConfig, Config}, - database::{Coin, DatabaseConnection, DatabaseInterface}, + database::{Coin, DatabaseConnection, DatabaseInterface, SpendBlock}, DaemonHandle, }; @@ -59,7 +59,7 @@ impl BitcoinInterface for DummyBitcoind { fn spent_coins( &self, _: &[(bitcoin::OutPoint, bitcoin::Txid)], - ) -> Vec<(bitcoin::OutPoint, bitcoin::Txid, u32)> { + ) -> Vec<(bitcoin::OutPoint, bitcoin::Txid, i32, u32)> { Vec::new() } @@ -164,19 +164,22 @@ impl DatabaseConnection for DummyDbConn { let mut db = self.db.write().unwrap(); let spent = &mut db.coins.get_mut(op).unwrap(); assert!(spent.spend_txid.is_none()); - assert!(spent.spend_block_time.is_none()); + assert!(spent.spend_block.is_none()); spent.spend_txid = Some(*spend_txid); } } - fn confirm_spend<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid, u32)]) { - for (op, spend_txid, time) in outpoints { + fn confirm_spend<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid, i32, u32)]) { + for (op, spend_txid, height, time) in outpoints { let mut db = self.db.write().unwrap(); let spent = &mut db.coins.get_mut(op).unwrap(); assert!(spent.spend_txid.is_some()); - assert!(spent.spend_block_time.is_none()); + assert!(spent.spend_block.is_none()); spent.spend_txid = Some(*spend_txid); - spent.spend_block_time = Some(*time); + spent.spend_block = Some(SpendBlock { + height: *height, + time: *time, + }); } }