db: require the spend block height from the DB interface

Hence add a 'spend_block_height' field to the 'coin' column in the
SQLite implementation. This also contains a couple cleanups, as well as
a fix (we were still checking if the blockheight was > 1).
This commit is contained in:
Antoine Poinsot 2022-10-14 09:44:52 +02:00
parent 6038843d33
commit 972c8dac86
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304
7 changed files with 91 additions and 42 deletions

View File

@ -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<bitcoin::Txid, Option<d::GetTxRes>> = HashMap::new();
@ -174,10 +174,15 @@ impl BitcoinInterface for d::BitcoinD {
let mut txs_to_cache: Vec<(bitcoin::Txid, Option<d::GetTxRes>)> = 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<sync::Mutex<dyn BitcoinInterface + 'static>>
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)
}

View File

@ -15,7 +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)>,
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);
}

View File

@ -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,
},
]);

View File

@ -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<DbSpendBlock> 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<bitcoin::Txid>,
pub spend_block_time: Option<u32>,
pub spend_block: Option<SpendBlock>,
}
impl std::convert::From<DbCoin> for Coin {
@ -206,7 +221,7 @@ impl std::convert::From<DbCoin> for Coin {
amount,
derivation_index,
spend_txid,
spend_block_time,
spend_block,
..
} = db_coin;
Coin {
@ -216,7 +231,7 @@ impl std::convert::From<DbCoin> for Coin {
amount,
derivation_index,
spend_txid,
spend_block_time,
spend_block: spend_block.map(SpendBlock::from),
}
}
}

View File

@ -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<Item = &'a (bitcoin::OutPoint, bitcoin::Txid, u32)>,
outpoints: impl IntoIterator<Item = &'a (bitcoin::OutPoint, bitcoin::Txid, i32, u32)>,
) {
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<bitcoin::OutPoint> = 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<bitcoin::OutPoint> = 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();

View File

@ -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<bitcoin::Txid>,
pub spend_block_time: Option<u32>,
pub spend_block: Option<DbSpendBlock>,
}
impl TryFrom<&rusqlite::Row<'_>> for DbCoin {
@ -161,7 +171,13 @@ impl TryFrom<&rusqlite::Row<'_>> for DbCoin {
let spend_txid: Option<Vec<u8>> = 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<i32> = row.get(9)?;
let spend_time: Option<u32> = 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,
})
}
}

View File

@ -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,
});
}
}