From 39013b163490ff7f48943d09714ef24f2f9a4755 Mon Sep 17 00:00:00 2001 From: Vladimir Fomene Date: Wed, 17 Aug 2022 11:17:54 +0300 Subject: [PATCH 1/2] Add API to delete spent UTXOs from database We currently store spent UTXOs in the database with an `is_spent` field and we don't provide users with a way to delete these UTXOs. The issue here is that these could easily bloat the database or memory. This PR provides methods that can allow users to delete spent UTXOs from the database. This PR fixes issue #573 add verification --- src/database/mod.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/database/mod.rs b/src/database/mod.rs index e3e2b3310..66d06462e 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -158,6 +158,33 @@ pub trait Database: BatchOperations { /// /// It should insert and return `0` if not present in the database fn increment_last_index(&mut self, keychain: KeychainKind) -> Result; + + /// Delete a list of spent utxos from database. Delete all spent utxos if list is `None`. + fn del_spent_utxos( + &mut self, + to_delete: Option>, + ) -> Result, Error> { + if let Some(to_delete) = to_delete { + let to_delete = to_delete + .iter() + .filter_map(|out| self.get_utxo(out).transpose()) + .collect::, _>>()?; + let deleted_utxos = to_delete + .iter() + .filter(|utxo| utxo.is_spent) + .filter_map(|out| self.del_utxo(&out.outpoint).transpose()) + .collect::, _>>()?; + Ok(deleted_utxos) + } else { + let deleted_utxos = self + .iter_utxos()? + .iter() + .filter(|utxo| utxo.is_spent) + .filter_map(|out| self.del_utxo(&out.outpoint).transpose()) + .collect::, _>>()?; + Ok(deleted_utxos) + } + } } /// Trait for a database that supports batch operations From 3e4b2c95c30eb1589691c765f4b78afa8c8a8a19 Mon Sep 17 00:00:00 2001 From: Vladimir Fomene Date: Fri, 23 Sep 2022 08:27:55 +0300 Subject: [PATCH 2/2] Test delete spent utxos functionality This commit adds test for `del_spent_utxos`. --- src/database/keyvalue.rs | 5 ++ src/database/memory.rs | 5 ++ src/database/mod.rs | 130 +++++++++++++++++++++++++++++++++++++++ src/database/sqlite.rs | 5 ++ 4 files changed, 145 insertions(+) diff --git a/src/database/keyvalue.rs b/src/database/keyvalue.rs index f586ebeba..eb8f1633d 100644 --- a/src/database/keyvalue.rs +++ b/src/database/keyvalue.rs @@ -532,4 +532,9 @@ mod test { fn test_check_descriptor_checksum() { crate::database::test::test_check_descriptor_checksum(get_tree()); } + + #[test] + fn test_del_spent_utxos() { + crate::database::test::test_del_spent_utxos(get_tree()); + } } diff --git a/src/database/memory.rs b/src/database/memory.rs index 691e7eb16..2a845e1b2 100644 --- a/src/database/memory.rs +++ b/src/database/memory.rs @@ -687,4 +687,9 @@ mod test { fn test_check_descriptor_checksum() { crate::database::test::test_check_descriptor_checksum(get_tree()); } + + #[test] + fn test_del_spent_utxos() { + crate::database::test::test_del_spent_utxos(get_tree()); + } } diff --git a/src/database/mod.rs b/src/database/mod.rs index 66d06462e..784760205 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -680,5 +680,135 @@ pub mod test { assert!(res.is_err()); } + pub fn test_del_spent_utxos(mut db: D) { + // Query database for utxos, to prove that it is empty + assert_eq!(db.iter_utxos().unwrap().len(), 0); + + let (first_utxo, _second_utxo, third_utxo, fourth_utxo) = + setup_del_spent_utxo_test(&mut db); + // test that insertion was successful + assert_eq!(db.iter_utxos().unwrap().len(), 4); + + // call del_spent_utxos with None + let mut res = db.del_spent_utxos(None).unwrap(); + res.sort_by(|a, b| a.txout.value.cmp(&b.txout.value)); + // verify that the spent ones has been deleted + assert_eq!( + res, + vec![first_utxo.clone(), third_utxo.clone(), fourth_utxo.clone()] + ); + assert_eq!(db.iter_utxos().unwrap().len(), 1); + // re-insert the deleted utxo into database + db.set_utxo(&first_utxo).unwrap(); + db.set_utxo(&third_utxo).unwrap(); + db.set_utxo(&fourth_utxo).unwrap(); + + // call del_spent_utxos with vector of utxos + let mut res = db + .del_spent_utxos(Some(vec![first_utxo.outpoint, third_utxo.outpoint])) + .unwrap(); + res.sort_by(|a, b| a.txout.value.cmp(&b.txout.value)); + assert_eq!(res, vec![first_utxo.clone(), third_utxo.clone()]); + let utxos = db.iter_utxos().unwrap(); + assert_eq!(utxos.len(), 2); + assert!(utxos.contains(&fourth_utxo)); + } + + fn setup_del_spent_utxo_test( + db: &mut D, + ) -> (LocalUtxo, LocalUtxo, LocalUtxo, LocalUtxo) { + // insert four utxos into database + let first_outpoint = OutPoint::from_str( + "c1b4e695098210a31fe02abffe9005cffc051bbe86ff33e173155bcbdc5821e3:0", + ) + .unwrap(); + let first_script = Script::from( + Vec::::from_hex("76a914db4d1141d0048b1ed15839d0b7a4c488cd368b0e88ac").unwrap(), + ); + let first_txout = TxOut { + value: 133742, + script_pubkey: first_script, + }; + let first_utxo = LocalUtxo { + txout: first_txout, + outpoint: first_outpoint, + keychain: KeychainKind::External, + is_spent: true, + }; + + db.set_utxo(&first_utxo).unwrap(); + + let second_outpoint = OutPoint::from_str( + "fc9e4f9c334d55c1dc535bd691a1c159b0f7314c54745522257a905e18a56779:1", + ) + .unwrap(); + + let second_script = Script::from( + Vec::::from_hex("76a914824d8a679134215d6d21d25bde3cc63f89ec92eb88ac").unwrap(), + ); + + let second_txout = TxOut { + value: 2257563, + script_pubkey: second_script, + }; + + let second_utxo = LocalUtxo { + txout: second_txout, + outpoint: second_outpoint, + keychain: KeychainKind::External, + is_spent: false, + }; + + db.set_utxo(&second_utxo).unwrap(); + + let third_outpoint = OutPoint::from_str( + "26e14e606105b250e64849cc14a484ce58f0f7f7064e662862001661b726427b:1", + ) + .unwrap(); + + let third_script = Script::from( + Vec::::from_hex("76a9140d5a9a6f7aae31ebb3b72bbfd05935de5765e0e688ac").unwrap(), + ); + + let third_txout = TxOut { + value: 1119096, + script_pubkey: third_script, + }; + + let third_utxo = LocalUtxo { + txout: third_txout, + outpoint: third_outpoint, + keychain: KeychainKind::External, + is_spent: true, + }; + + db.set_utxo(&third_utxo).unwrap(); + + let fourth_outpoint = OutPoint::from_str( + "d27cff02d45817a11ac01d0d93a2e439f2d643b5d8bc57f4f7f56aa571104e8c:0", + ) + .unwrap(); + + let fourth_script = Script::from( + Vec::::from_hex("76a914899490496bfd1228e7ad5b0e156f1fc50a8f6f7688ac").unwrap(), + ); + + let fourth_txout = TxOut { + value: 36662433, + script_pubkey: fourth_script, + }; + + let fourth_utxo = LocalUtxo { + txout: fourth_txout, + outpoint: fourth_outpoint, + keychain: KeychainKind::External, + is_spent: true, + }; + + db.set_utxo(&fourth_utxo).unwrap(); + + (first_utxo, second_utxo, third_utxo, fourth_utxo) + } + // TODO: more tests... } diff --git a/src/database/sqlite.rs b/src/database/sqlite.rs index a8061984f..de6fdc722 100644 --- a/src/database/sqlite.rs +++ b/src/database/sqlite.rs @@ -1096,4 +1096,9 @@ pub mod test { fn test_check_descriptor_checksum() { crate::database::test::test_check_descriptor_checksum(get_database()); } + + #[test] + fn test_del_spent_utxos() { + crate::database::test::test_del_spent_utxos(get_database()); + } }