db: database interface to rollback to a previous best block

This commit is contained in:
Antoine Poinsot 2022-10-14 10:49:12 +02:00
parent 972c8dac86
commit e9e4acd69d
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304
4 changed files with 237 additions and 13 deletions

View File

@ -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<DbSpendBlock> 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<i32>,

View File

@ -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<bitcoin::OutPoint> = 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::<Vec<_>>(),
);
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::<Vec<_>>(),
);
let mut db_coins = conn
.db_coins(
&coins
.iter()
.map(|c| c.outpoint)
.collect::<Vec<bitcoin::OutPoint>>(),
)
.into_iter()
.map(Coin::from)
.collect::<Vec<_>>();
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::<Vec<bitcoin::OutPoint>>(),
)
.into_iter()
.map(|c| (c.outpoint, Coin::from(c)))
.collect::<HashMap<_, _>>();
// 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();
}
}

View File

@ -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,

View File

@ -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 {