From e9e4acd69de55e0c32c7e275e3b5318b5e161c46 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Fri, 14 Oct 2022 10:49:12 +0200 Subject: [PATCH] db: database interface to rollback to a previous best block --- src/database/mod.rs | 11 +- src/database/sqlite/mod.rs | 223 +++++++++++++++++++++++++++++++++- src/database/sqlite/schema.rs | 4 +- src/testutils.rs | 12 +- 4 files changed, 237 insertions(+), 13 deletions(-) diff --git a/src/database/mod.rs b/src/database/mod.rs index b356c9ed..1568bdca 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -88,6 +88,9 @@ pub trait DatabaseConnection { /// Delete a Spend transaction from database. fn delete_spend(&mut self, txid: &bitcoin::Txid); + + /// Mark the given tip as the new best seen block. Update stored data accordingly. + fn rollback_tip(&mut self, new_tip: &BlockChainTip); } impl DatabaseConnection for SqliteConn { @@ -184,9 +187,13 @@ impl DatabaseConnection for SqliteConn { fn delete_spend(&mut self, txid: &bitcoin::Txid) { self.delete_spend(txid) } + + fn rollback_tip(&mut self, new_tip: &BlockChainTip) { + self.rollback_tip(new_tip) + } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct SpendBlock { pub height: i32, pub time: u32, @@ -201,7 +208,7 @@ impl From for SpendBlock { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Coin { pub outpoint: bitcoin::OutPoint, pub block_height: Option, diff --git a/src/database/sqlite/mod.rs b/src/database/sqlite/mod.rs index b9f410b9..fd953d39 100644 --- a/src/database/sqlite/mod.rs +++ b/src/database/sqlite/mod.rs @@ -439,13 +439,47 @@ impl SqliteConn { }) .expect("Db must not fail"); } + + /// Unconfirm all data that was marked as being confirmed *after* the given chain + /// tip, and set it as our new best block seen. + /// + /// This includes: + /// - Coins + /// - Spending transactions confirmation + /// - Tip + /// + /// This will have to be updated if we are to add new fields based on block data + /// in the database eventually. + pub fn rollback_tip(&mut self, new_tip: &BlockChainTip) { + db_exec(&mut self.conn, |db_tx| { + db_tx.execute( + "UPDATE coins SET blockheight = NULL, blocktime = NULL, spend_block_height = NULL, spend_block_time = NULL WHERE blockheight > ?1", + rusqlite::params![new_tip.height], + )?; + db_tx.execute( + "UPDATE coins SET spend_block_height = NULL, spend_block_time = NULL WHERE spend_block_height > ?1", + rusqlite::params![new_tip.height], + )?; + db_tx.execute( + "UPDATE tip SET blockheight = (?1), blockhash = (?2)", + rusqlite::params![new_tip.height, new_tip.hash.to_vec()], + )?; + Ok(()) + }) + .expect("Db must not fail"); + } } #[cfg(test)] mod tests { use super::*; + use crate::database::SpendBlock; use crate::testutils::*; - use std::{collections::HashSet, fs, path, str::FromStr}; + use std::{ + collections::{HashMap, HashSet}, + fs, path, + str::FromStr, + }; use bitcoin::{hashes::Hash, util::bip32}; @@ -570,7 +604,7 @@ mod tests { spend_txid: None, spend_block: None, }; - conn.new_unspent_coins(&[coin_a.clone()]); // On 1.48, arrays aren't IntoIterator + conn.new_unspent_coins(&[coin_a]); assert_eq!(conn.unspent_coins()[0].outpoint, coin_a.outpoint); // We can query it by its outpoint @@ -591,7 +625,7 @@ mod tests { spend_txid: None, spend_block: None, }; - conn.new_unspent_coins(&[coin_b.clone()]); + conn.new_unspent_coins(&[coin_b]); let outpoints: HashSet = conn .unspent_coins() .into_iter() @@ -711,4 +745,187 @@ mod tests { fs::remove_dir_all(&tmp_dir).unwrap(); } + + #[test] + fn sqlite_tip_rollback() { + let (tmp_dir, _, _, db) = dummy_db(); + + { + let mut conn = db.connection().unwrap(); + + let old_tip = BlockChainTip { + hash: bitcoin::BlockHash::from_str( + "00000000000000000004f43b5e743757939082170673d27a5a5130e0eb238832", + ) + .unwrap(), + height: 200_000, + }; + conn.update_tip(&old_tip); + + // 5 coins: + // - One unconfirmed + // - One confirmed before the rollback height + // - One confirmed before the rollback height but spent after + // - One confirmed after the rollback height + // - One spent after the rollback height + let coins = [ + Coin { + outpoint: bitcoin::OutPoint::from_str( + "6f0dc85a369b44458eba3a1f0ea5b5935d563afb6994f70f5b0094e05be1676c:1", + ) + .unwrap(), + block_height: None, + block_time: None, + amount: bitcoin::Amount::from_sat(98765), + derivation_index: bip32::ChildNumber::from_normal_idx(10).unwrap(), + spend_txid: None, + spend_block: None, + }, + Coin { + outpoint: bitcoin::OutPoint::from_str( + "c449539458c60bee6c0d8905ba1dadb20b9187b82045d306a408b894cea492b0:2", + ) + .unwrap(), + block_height: Some(101_095), + block_time: Some(1_111_899), + amount: bitcoin::Amount::from_sat(98765), + derivation_index: bip32::ChildNumber::from_normal_idx(100).unwrap(), + spend_txid: None, + spend_block: None, + }, + Coin { + outpoint: bitcoin::OutPoint::from_str( + "f0801fd9ca8bca0624c230ab422b2e2c4c8dc995e4e1dbc6412510959cce1e4f:3", + ) + .unwrap(), + block_height: Some(101_099), + block_time: Some(1_121_899), + amount: bitcoin::Amount::from_sat(98765), + derivation_index: bip32::ChildNumber::from_normal_idx(1000).unwrap(), + spend_txid: Some( + bitcoin::Txid::from_str( + "0c62a990d20d54429e70859292e82374ba6b1b951a3ab60f26bb65fee5724ff7", + ) + .unwrap(), + ), + spend_block: Some(SpendBlock { + height: 101_199, + time: 1_231_678, + }), + }, + Coin { + outpoint: bitcoin::OutPoint::from_str( + "19f56e65069f0a7a3bfb00c6a7085cc0669e03e91befeca1ee9891c9e737b2fb:4", + ) + .unwrap(), + block_height: Some(101_100), + block_time: Some(1_131_899), + amount: bitcoin::Amount::from_sat(98765), + derivation_index: bip32::ChildNumber::from_normal_idx(10000).unwrap(), + spend_txid: None, + spend_block: None, + }, + Coin { + outpoint: bitcoin::OutPoint::from_str( + "ed6c8f1af9325f84de521e785e7ddfd33dc28c9ada4d687dcd3850100bde54e9:5", + ) + .unwrap(), + block_height: Some(101_102), + block_time: Some(1_134_899), + amount: bitcoin::Amount::from_sat(98765), + derivation_index: bip32::ChildNumber::from_normal_idx(100000).unwrap(), + spend_txid: Some( + bitcoin::Txid::from_str( + "7477017f992cdc7ba08acafb77cb3b5bc0f42ac340d3e1e1da0785bdda20d5f6", + ) + .unwrap(), + ), + spend_block: Some(SpendBlock { + height: 101_105, + time: 1_201_678, + }), + }, + ]; + conn.new_unspent_coins(&coins); + conn.confirm_coins( + &coins + .iter() + .filter_map(|c| { + c.block_height + .map(|b| (c.outpoint, b, c.block_time.unwrap())) + }) + .collect::>(), + ); + conn.confirm_spend( + &coins + .iter() + .filter_map(|c| { + c.spend_block + .as_ref() + .map(|b| (c.outpoint, c.spend_txid.unwrap(), b.height, b.time)) + }) + .collect::>(), + ); + let mut db_coins = conn + .db_coins( + &coins + .iter() + .map(|c| c.outpoint) + .collect::>(), + ) + .into_iter() + .map(Coin::from) + .collect::>(); + db_coins.sort_by(|c1, c2| c1.outpoint.vout.cmp(&c2.outpoint.vout)); + assert_eq!(&db_coins[..], &coins[..]); + + // Now that everything is settled, reorg to a previous height. + let new_tip = BlockChainTip { + hash: bitcoin::BlockHash::from_str( + "000000000000000000016440c591da27679abfa53ef44d45b016640dbd04e126", + ) + .unwrap(), + height: 101_099, + }; + conn.rollback_tip(&new_tip); + + // The tip got updated + let new_db_tip = conn.db_tip(); + assert_eq!(new_db_tip.block_height.unwrap(), new_tip.height); + assert_eq!(new_db_tip.block_hash.unwrap(), new_tip.hash); + + // And so were the coins + let db_coins = conn + .db_coins( + &coins + .iter() + .map(|c| c.outpoint) + .collect::>(), + ) + .into_iter() + .map(|c| (c.outpoint, Coin::from(c))) + .collect::>(); + // The first coin is unchanged + assert_eq!(db_coins[&coins[0].outpoint], coins[0]); + // Same for the second one + assert_eq!(db_coins[&coins[1].outpoint], coins[1]); + // The third one got its spend confirmation info wiped, but only that + let mut coin = coins[2]; + coin.spend_block = None; + assert_eq!(db_coins[&coins[2].outpoint], coin); + // The fourth one got its own confirmation info wiped + let mut coin = coins[3]; + coin.block_height = None; + coin.block_time = None; + assert_eq!(db_coins[&coins[3].outpoint], coin); + // The fourth one got both is own confirmation and spend confirmation info wiped + let mut coin = coins[4]; + coin.block_height = None; + coin.block_time = None; + coin.spend_block = None; + assert_eq!(db_coins[&coins[4].outpoint], coin); + } + + fs::remove_dir_all(&tmp_dir).unwrap(); + } } diff --git a/src/database/sqlite/schema.rs b/src/database/sqlite/schema.rs index ed71476e..58b5f8b5 100644 --- a/src/database/sqlite/schema.rs +++ b/src/database/sqlite/schema.rs @@ -130,13 +130,13 @@ impl TryFrom<&rusqlite::Row<'_>> for DbWallet { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct DbSpendBlock { pub height: i32, pub time: u32, } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct DbCoin { pub id: i64, pub wallet_id: i64, diff --git a/src/testutils.rs b/src/testutils.rs index cdb5e9ce..a8fae617 100644 --- a/src/testutils.rs +++ b/src/testutils.rs @@ -132,7 +132,7 @@ impl DatabaseConnection for DummyDbConn { let mut result = HashMap::new(); for (k, v) in self.db.read().unwrap().coins.iter() { if v.spend_txid.is_some() { - result.insert(*k, v.clone()); + result.insert(*k, *v); } } result @@ -140,11 +140,7 @@ impl DatabaseConnection for DummyDbConn { fn new_unspent_coins<'a>(&mut self, coins: &[Coin]) { for coin in coins { - self.db - .write() - .unwrap() - .coins - .insert(coin.outpoint, coin.clone()); + self.db.write().unwrap().coins.insert(coin.outpoint, *coin); } } @@ -228,6 +224,10 @@ impl DatabaseConnection for DummyDbConn { fn delete_spend(&mut self, txid: &bitcoin::Txid) { self.db.write().unwrap().spend_txs.remove(txid); } + + fn rollback_tip(&mut self, _: &BlockChainTip) { + todo!() + } } pub struct DummyMinisafe {