From 8c4581abe8e3a363c3420a64b753f2d3892838de Mon Sep 17 00:00:00 2001 From: sandyp025 Date: Fri, 3 Apr 2026 01:33:07 +0530 Subject: [PATCH] fix(watch-only): make AddressCache::new fallible and replace panics with typed errors Add proper error handling for save, update, and lock operations in AddressCache. --- .../src/electrum_protocol.rs | 172 +++--- crates/floresta-node/src/florestad.rs | 7 +- .../floresta-node/src/json_rpc/blockchain.rs | 27 +- crates/floresta-node/src/json_rpc/server.rs | 12 +- crates/floresta-watch-only/src/kv_database.rs | 43 +- crates/floresta-watch-only/src/lib.rs | 511 +++++++++++------- .../src/memory_database.rs | 37 +- crates/floresta/examples/watch-only.rs | 31 +- 8 files changed, 520 insertions(+), 320 deletions(-) diff --git a/crates/floresta-electrum/src/electrum_protocol.rs b/crates/floresta-electrum/src/electrum_protocol.rs index 775123001..68843412a 100644 --- a/crates/floresta-electrum/src/electrum_protocol.rs +++ b/crates/floresta-electrum/src/electrum_protocol.rs @@ -310,7 +310,10 @@ impl ElectrumServer { "blockchain.relayfee" => json_rpc_res!(request, 0.00001), "blockchain.scripthash.get_balance" => { let script_hash = get_arg!(request, sha256::Hash, 0); - let balance = self.address_cache.get_address_balance(&script_hash); + let balance = self + .address_cache + .get_address_balance(&script_hash) + .map_err(|e| super::error::Error::Blockchain(Box::new(e)))?; let result = json!({ "confirmed": balance, "unconfirmed": 0 @@ -319,32 +322,35 @@ impl ElectrumServer { } "blockchain.scripthash.get_history" => { let script_hash = get_arg!(request, sha256::Hash, 0); - self.address_cache + let transactions = self + .address_cache .get_address_history(&script_hash) - .map(|transactions| { - let res = Self::process_history(&transactions); - json_rpc_res!(request, res) - }) - .unwrap_or_else(|| { - Ok(json!({ - "jsonrpc": "2.0", - "result": [], - "id": request.id - })) - }) + .map_err(|e| super::error::Error::Blockchain(Box::new(e)))?; + let res = Self::process_history(&transactions); + json_rpc_res!(request, res) } "blockchain.scripthash.get_mempool" => json_rpc_res!(request, []), "blockchain.scripthash.listunspent" => { let hash = get_arg!(request, sha256::Hash, 0); - let utxos = self.address_cache.get_address_utxos(&hash); - if utxos.is_none() { + let utxos = self + .address_cache + .get_address_utxos(&hash) + .map_err(|e| super::error::Error::Blockchain(Box::new(e)))?; + if utxos.is_empty() { return json_rpc_res!(request, []); } let mut final_utxos = Vec::new(); - for (utxo, prevout) in utxos.unwrap().into_iter() { - let height = self.address_cache.get_height(&prevout.txid).unwrap(); - - let position = self.address_cache.get_position(&prevout.txid).unwrap(); + for (utxo, prevout) in utxos.into_iter() { + let height = self + .address_cache + .get_height(&prevout.txid) + .map_err(|e| super::error::Error::Blockchain(Box::new(e)))? + .unwrap_or(0); + let position = self + .address_cache + .get_position(&prevout.txid) + .map_err(|e| super::error::Error::Blockchain(Box::new(e)))? + .unwrap_or(0); final_utxos.push(json!({ "height": height, @@ -362,7 +368,7 @@ impl ElectrumServer { let history = self.address_cache.get_address_history(&hash); match history { - Some(transactions) if !transactions.is_empty() => { + Ok(transactions) if !transactions.is_empty() => { let res = get_status(transactions); json_rpc_res!(request, res) } @@ -382,8 +388,14 @@ impl ElectrumServer { let script = get_arg!(request, ScriptBuf, 0); let hash = get_spk_hash(&script); - if !self.address_cache.is_address_cached(&hash) { - self.address_cache.cache_address(script.clone()); + if !self + .address_cache + .is_address_cached(&hash) + .map_err(|e| super::error::Error::Blockchain(Box::new(e)))? + { + self.address_cache + .cache_address(script.clone()) + .map_err(|e| super::error::Error::Blockchain(Box::new(e)))?; self.addresses_to_scan.push(script); let res = json!({ "confirmed": 0, @@ -392,7 +404,10 @@ impl ElectrumServer { return json_rpc_res!(request, res); } - let balance = self.address_cache.get_address_balance(&hash); + let balance = self + .address_cache + .get_address_balance(&hash) + .map_err(|e| super::error::Error::Blockchain(Box::new(e)))?; let result = json!({ "confirmed": balance, "unconfirmed": 0 @@ -403,25 +418,24 @@ impl ElectrumServer { let script = get_arg!(request, ScriptBuf, 0); let hash = get_spk_hash(&script); - if !self.address_cache.is_address_cached(&hash) { - self.address_cache.cache_address(script.clone()); + if !self + .address_cache + .is_address_cached(&hash) + .map_err(|e| super::error::Error::Blockchain(Box::new(e)))? + { + self.address_cache + .cache_address(script.clone()) + .map_err(|e| super::error::Error::Blockchain(Box::new(e)))?; self.addresses_to_scan.push(script); return json_rpc_res!(request, null); } - self.address_cache + let transactions = self + .address_cache .get_address_history(&hash) - .map(|transactions| { - let res = Self::process_history(&transactions); - json_rpc_res!(request, res) - }) - .unwrap_or_else(|| { - Ok(json!({ - "jsonrpc": "2.0", - "result": null, - "id": request.id - })) - }) + .map_err(|e| super::error::Error::Blockchain(Box::new(e)))?; + let res = Self::process_history(&transactions); + json_rpc_res!(request, res) } "blockchain.scriptpubkey.subscribe" => { let script = get_arg!(request, ScriptBuf, 0); @@ -430,14 +444,14 @@ impl ElectrumServer { let history = self.address_cache.get_address_history(&hash); match history { - Some(transactions) if !transactions.is_empty() => { + Ok(transactions) if !transactions.is_empty() => { let res = get_status(transactions); json_rpc_res!(request, res) } - Some(_) => { + Ok(_) => { json_rpc_res!(request, null) } - None => { + Err(_) => { self.addresses_to_scan.push(script); json_rpc_res!(request, null) } @@ -468,19 +482,27 @@ impl ElectrumServer { return Err(super::error::Error::Mempool(Box::new(e))); }; - let updated = self - .address_cache - .cache_mempool_transaction(&tx) - .into_iter() - .map(|spend| (tx.clone(), spend)) - .collect::>(); + let updated = match self.address_cache.cache_mempool_transaction(&tx) { + Ok(coins) => coins + .into_iter() + .map(|spend| (tx.clone(), spend)) + .collect::>(), + Err(e) => { + error!("Error caching mempool transaction: {e}"); + vec![] + } + }; self.wallet_notify(&updated); json_rpc_res!(request, txid) } "blockchain.transaction.get" => { let tx_id = get_arg!(request, Txid, 0); - let tx = self.address_cache.get_cached_transaction(&tx_id); + let tx = self + .address_cache + .get_cached_transaction(&tx_id) + .ok() + .flatten(); if let Some(tx) = tx { return json_rpc_res!(request, tx); } @@ -489,8 +511,8 @@ impl ElectrumServer { } "blockchain.transaction.get_merkle" => { let tx_id = get_arg!(request, Txid, 0); - let proof = self.address_cache.get_merkle_proof(&tx_id); - let height = self.address_cache.get_height(&tx_id); + let proof = self.address_cache.get_merkle_proof(&tx_id).ok().flatten(); + let height = self.address_cache.get_height(&tx_id).ok().flatten(); if let Some(proof) = proof { let result = json!({ "merkle": proof.hashes, @@ -595,7 +617,9 @@ impl ElectrumServer { } self.addresses_to_scan.iter().for_each(|address| { - self.address_cache.cache_address(address.clone()); + if let Err(e) = self.address_cache.cache_address(address.clone()) { + error!("Could not cache address: {e}"); + } }); info!("Catching up with addresses {:?}", self.addresses_to_scan); @@ -702,10 +726,12 @@ impl ElectrumServer { }] }); - let current_height = self.address_cache.get_cache_height(); + let current_height = self.address_cache.get_cache_height().unwrap_or(0); if (!self.chain.is_in_ibd() || height % 1000 == 0) && (height > current_height) { - self.address_cache.bump_height(height); + if let Err(e) = self.address_cache.bump_height(height) { + error!("Could not update cache height: {e}"); + } } if self.chain.get_height().unwrap() == height { @@ -717,7 +743,13 @@ impl ElectrumServer { } } - let transactions = self.address_cache.block_process(&block, height); + let transactions = match self.address_cache.block_process(&block, height) { + Ok(txs) => txs, + Err(e) => { + error!("Error processing block at height {height}: {e}"); + vec![] + } + }; self.wallet_notify(&transactions); } @@ -813,9 +845,15 @@ impl ElectrumServer { for (_, out) in transactions { let hash = get_spk_hash(&out.script_pubkey); if let Some(client) = self.client_addresses.get(&hash) { - let history = self.address_cache.get_address_history(&hash); + let history = match self.address_cache.get_address_history(&hash) { + Ok(h) => h, + Err(e) => { + error!("{e}"); + continue; + } + }; - let status_hash = get_status(history.unwrap()); + let status_hash = get_status(history); let notify = json!({ "jsonrpc": "2.0", "method": "blockchain.scripthash.subscribe", @@ -1022,20 +1060,22 @@ mod test { fn get_test_cache() -> Arc> { let test_id: u32 = rand::random(); let cache = KvDatabase::new(format!("./tmp-db/{test_id}.floresta")).unwrap(); - let cache = AddressCache::new(cache); + let cache = AddressCache::new(cache).unwrap(); // Inserting test transactions in the wallet let (transaction, proof) = get_test_transaction(); - cache.cache_transaction( - &transaction, - 118511, - transaction.output[0].value.to_sat(), - proof, - 1, - 0, - false, - get_spk_hash(&transaction.output[0].script_pubkey), - ); + cache + .cache_transaction( + &transaction, + 118511, + transaction.output[0].value.to_sat(), + proof, + 1, + 0, + false, + get_spk_hash(&transaction.output[0].script_pubkey), + ) + .unwrap(); Arc::new(cache) } diff --git a/crates/floresta-node/src/florestad.rs b/crates/floresta-node/src/florestad.rs index 1501df151..c77514b27 100644 --- a/crates/floresta-node/src/florestad.rs +++ b/crates/floresta-node/src/florestad.rs @@ -715,7 +715,8 @@ impl Florestad { let database = KvDatabase::new(self.config.data_dir.clone()) .map_err(FlorestadError::CouldNotOpenKvDatabase)?; - let wallet = AddressCache::new(database); + let wallet = + AddressCache::new(database).map_err(FlorestadError::CouldNotInitializeWallet)?; wallet .setup() @@ -744,7 +745,9 @@ impl Florestad { } for address in self.get_addresses()? { - wallet.cache_address(address); + wallet + .cache_address(address) + .map_err(|e| FlorestadError::CouldNotSetupWallet(e.to_string()))?; } info!("Wallet setup completed!"); diff --git a/crates/floresta-node/src/json_rpc/blockchain.rs b/crates/floresta-node/src/json_rpc/blockchain.rs index a33e75484..f69d59ad4 100644 --- a/crates/floresta-node/src/json_rpc/blockchain.rs +++ b/crates/floresta-node/src/json_rpc/blockchain.rs @@ -23,6 +23,7 @@ use miniscript::descriptor::checksum; use serde_json::json; use serde_json::Value; use tracing::debug; +use tracing::error; use super::res::GetBlockchainInfoRes; use super::res::GetTxOutProof; @@ -52,6 +53,8 @@ impl RpcImpl { let height = self .wallet .get_height(txid) + .ok() + .flatten() .ok_or(JsonRpcError::TxNotFound)?; let blockhash = self.chain.get_block_hash(height).unwrap(); self.chain @@ -476,12 +479,14 @@ impl RpcImpl { _include_mempool: bool, ) -> Result, JsonRpcError> { let res = match ( - self.wallet.get_transaction(&txid), - self.wallet.get_height(&txid), - self.wallet.get_utxo(&OutPoint { - txid, - vout: outpoint, - }), + self.wallet.get_transaction(&txid).ok().flatten(), + self.wallet.get_height(&txid).ok().flatten(), + self.wallet + .get_utxo(&OutPoint { + txid, + vout: outpoint, + }) + .ok(), ) { (Some(cached_tx), Some(height), Some(txout)) => { let is_coinbase = cached_tx.tx.is_coinbase(); @@ -599,7 +604,7 @@ impl RpcImpl { script: ScriptBuf, height: u32, ) -> Result { - if let Some(txout) = self.wallet.get_utxo(&OutPoint { txid, vout }) { + if let Ok(txout) = self.wallet.get_utxo(&OutPoint { txid, vout }) { return Ok(serde_json::to_value(txout).unwrap()); } @@ -613,7 +618,9 @@ impl RpcImpl { return Err(JsonRpcError::NoBlockFilters); }; - self.wallet.cache_address(script.clone()); + if let Err(e) = self.wallet.cache_address(script.clone()) { + error!("Could not cache address: {e}"); + } let filter_key = script.to_bytes(); let candidates = cfilters .match_any( @@ -642,7 +649,9 @@ impl RpcImpl { return Err(JsonRpcError::BlockNotFound); }; - self.wallet.block_process(&candidate, height); + if let Err(e) = self.wallet.block_process(&candidate, height) { + error!("Error processing block at height {height}: {e}"); + } } let val = match self.get_tx_out(txid, vout, false)? { diff --git a/crates/floresta-node/src/json_rpc/server.rs b/crates/floresta-node/src/json_rpc/server.rs index 32c87c25b..d154bba7c 100644 --- a/crates/floresta-node/src/json_rpc/server.rs +++ b/crates/floresta-node/src/json_rpc/server.rs @@ -91,12 +91,16 @@ impl RpcImpl { let tx = self .wallet .get_transaction(&tx_id) + .ok() + .flatten() .ok_or(JsonRpcError::TxNotFound); return tx.map(|tx| serde_json::to_value(self.make_raw_transaction(tx)).unwrap()); } self.wallet .get_transaction(&tx_id) + .ok() + .flatten() .and_then(|tx| serde_json::to_value(self.make_raw_transaction(tx)).ok()) .ok_or(JsonRpcError::TxNotFound) } @@ -106,7 +110,7 @@ impl RpcImpl { info!("Descriptor pushed: {descriptor}"); debug!("Rescanning with block filters for addresses: {addresses:?}"); - let addresses = self.wallet.get_cached_addresses(); + let addresses = self.wallet.get_cached_addresses().unwrap_or_default(); let wallet = self.wallet.clone(); if self.block_filter_storage.is_none() { return Err(JsonRpcError::InInitialBlockDownload); @@ -143,7 +147,7 @@ impl RpcImpl { return Err(JsonRpcError::InInitialBlockDownload); } - let addresses = self.wallet.get_cached_addresses(); + let addresses = self.wallet.get_cached_addresses().unwrap_or_default(); if addresses.is_empty() { return Err(JsonRpcError::NoAddressesToRescan); @@ -594,7 +598,9 @@ impl RpcImpl { .unwrap() .unwrap(); - wallet.block_process(&block, height); + if let Err(e) = wallet.block_process(&block, height) { + error!("Error processing block at height {height}: {e}"); + } } } diff --git a/crates/floresta-watch-only/src/kv_database.rs b/crates/floresta-watch-only/src/kv_database.rs index feabf0af3..bc72841a7 100644 --- a/crates/floresta-watch-only/src/kv_database.rs +++ b/crates/floresta-watch-only/src/kv_database.rs @@ -8,6 +8,7 @@ use core::fmt::Formatter; use bitcoin::consensus::deserialize; use bitcoin::consensus::encode::Error as EncodingError; use bitcoin::consensus::serialize; +use bitcoin::hashes::FromSliceError; use bitcoin::hashes::Hash; use bitcoin::Txid; use floresta_common::impl_error_from; @@ -37,11 +38,12 @@ pub enum KvDatabaseError { SerdeJsonError(serde_json::Error), WalletNotInitialized, DeserializeError(EncodingError), - TransactionNotFound, + InvalidTxid(FromSliceError), } impl_error_from!(KvDatabaseError, serde_json::Error, SerdeJsonError); impl_error_from!(KvDatabaseError, kv::Error, KvError); impl_error_from!(KvDatabaseError, EncodingError, DeserializeError); +impl_error_from!(KvDatabaseError, FromSliceError, InvalidTxid); impl Display for KvDatabaseError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { @@ -50,7 +52,7 @@ impl Display for KvDatabaseError { KvDatabaseError::SerdeJsonError(e) => write!(f, "SerdeJsonError: {e}"), KvDatabaseError::WalletNotInitialized => write!(f, "WalletNotInitialized"), KvDatabaseError::DeserializeError(e) => write!(f, "DeserializeError: {e}"), - KvDatabaseError::TransactionNotFound => write!(f, "TransactionNotFound"), + KvDatabaseError::InvalidTxid(e) => write!(f, "InvalidTxid: {e}"), } } } @@ -69,23 +71,21 @@ impl AddressCacheDatabase for KvDatabase { if *"height" == key || *"desc" == key { continue; } - let value: Vec = item.value().unwrap(); + let value: Vec = item.value()?; let value = serde_json::from_slice(&value)?; addresses.push(value); } Ok(addresses) } - fn save(&self, address: &super::CachedAddress) { + fn save(&self, address: &super::CachedAddress) -> Result<()> { let key = address.script_hash.to_string(); - let value = serde_json::to_vec(&address).expect("Invalid object serialization"); - - self.1 - .set(&key, &value) - .expect("Fatal: Database isn't working"); - self.1.flush().expect("Could not write to disk"); + let value = serde_json::to_vec(&address)?; + self.1.set(&key, &value)?; + self.1.flush()?; + Ok(()) } - fn update(&self, address: &super::CachedAddress) { - self.save(address); + fn update(&self, address: &super::CachedAddress) -> Result<()> { + self.save(address) } fn get_cache_height(&self) -> Result { let height = self.1.get(&String::from("height"))?; @@ -104,7 +104,7 @@ impl AddressCacheDatabase for KvDatabase { let mut descs = self.descs_get()?; descs.push(String::from(descriptor)); self.1 - .set(&String::from("desc"), &serde_json::to_vec(&descs).unwrap())?; + .set(&String::from("desc"), &serde_json::to_vec(&descs)?)?; self.1.flush()?; Ok(()) @@ -118,13 +118,13 @@ impl AddressCacheDatabase for KvDatabase { Ok(Vec::new()) } - fn get_transaction(&self, txid: &bitcoin::Txid) -> Result { + fn get_transaction(&self, txid: &bitcoin::Txid) -> Result> { let store = self.0.bucket::<&[u8], Vec>(Some("transactions"))?; let res = store.get(&txid.as_byte_array().to_vec().as_slice())?; if let Some(res) = res { - return Ok(serde_json::de::from_slice(&res)?); + return Ok(Some(serde_json::de::from_slice(&res)?)); } - Err(KvDatabaseError::TransactionNotFound) + Ok(None) } fn save_transaction(&self, tx: &super::CachedTransaction) -> Result<()> { @@ -145,7 +145,7 @@ impl AddressCacheDatabase for KvDatabase { if let Some(res) = res { return Ok(serde_json::de::from_slice(&res)?); } - Err(KvDatabaseError::TransactionNotFound) + Err(KvDatabaseError::WalletNotInitialized) } fn save_stats(&self, stats: &Stats) -> Result<()> { @@ -164,7 +164,7 @@ impl AddressCacheDatabase for KvDatabase { for item in store.iter() { let item = item?; let key = item.key::<&[u8]>()?; - transactions.push(Txid::from_slice(key).unwrap()); + transactions.push(Txid::from_slice(key)?); } Ok(transactions) } @@ -245,7 +245,10 @@ mod test { assert_eq!(db.get_stats().unwrap().address_count, 11); db.save_transaction(&cache_tx).unwrap(); - assert_eq!(db.get_transaction(&cache_tx.hash).unwrap(), cache_tx); + assert_eq!( + db.get_transaction(&cache_tx.hash).unwrap(), + Some(cache_tx.clone()) + ); assert_eq!(db.list_transactions().unwrap(), vec![cache_tx.hash]); db.set_cache_height(test_height).unwrap(); @@ -254,7 +257,7 @@ mod test { db.desc_save(desc).unwrap(); assert_eq!(db.descs_get().unwrap(), vec![desc]); - db.update(&cache_address); + db.update(&cache_address).unwrap(); assert_eq!(db.load().unwrap()[0].script_hash, cache_address.script_hash); } } diff --git a/crates/floresta-watch-only/src/lib.rs b/crates/floresta-watch-only/src/lib.rs index 2cc106d22..625c8c8d8 100644 --- a/crates/floresta-watch-only/src/lib.rs +++ b/crates/floresta-watch-only/src/lib.rs @@ -7,6 +7,7 @@ html_favicon_url = "https://raw.githubusercontent.com/getfloresta/floresta-media/master/logo_png/Icon-Green(main).png" )] #![allow(clippy::manual_is_multiple_of)] +#![cfg_attr(not(test), deny(clippy::unwrap_used))] use core::cmp::Ordering; use core::error::Error; @@ -60,6 +61,9 @@ const INDEX_INITIAL: u32 = 0; pub enum WatchOnlyError { WalletNotInitialized, TransactionNotFound, + CachedAddressNotFound, + IndexOutOfBounds, + PoisonedLock, DatabaseError(DatabaseError), DuplicateDescriptor(String), InvalidDescriptor(DescriptorError), @@ -74,6 +78,15 @@ impl Display for WatchOnlyError { WatchOnlyError::TransactionNotFound => { write!(f, "Transaction not found") } + WatchOnlyError::CachedAddressNotFound => { + write!(f, "Cached address not found in address map") + } + WatchOnlyError::IndexOutOfBounds => { + write!(f, "Index out of bounds") + } + WatchOnlyError::PoisonedLock => { + write!(f, "Poisoned lock") + } WatchOnlyError::DatabaseError(e) => { write!(f, "Database error: {e:?}") } @@ -129,7 +142,8 @@ impl Default for CachedTransaction { CachedTransaction { // A placeholder transaction with no input and no outputs, the bare-minimum to be // serializable - tx: deserialize(&Vec::from_hex("010000000000ffffffff").unwrap()).unwrap(), + tx: deserialize(&Vec::from_hex("010000000000ffffffff").expect("hardcoded valid hex")) + .expect("hardcoded valid transaction bytes"), height: 0, merkle_block: None, hash: Txid::all_zeros(), @@ -166,7 +180,7 @@ pub trait AddressCacheDatabase { type Error: Debug + Send + Sync + 'static; /// Saves a new address to the database. If the address already exists, `update` should /// be used instead - fn save(&self, address: &CachedAddress); + fn save(&self, address: &CachedAddress) -> Result<(), Self::Error>; /// Loads all addresses we have cached so far fn load(&self) -> Result, Self::Error>; /// Loads the data associated with our watch-only wallet. @@ -174,7 +188,7 @@ pub trait AddressCacheDatabase { /// Saves the data associated with our watch-only wallet. fn save_stats(&self, stats: &Stats) -> Result<(), Self::Error>; /// Updates an address, probably because a new transaction arrived - fn update(&self, address: &CachedAddress); + fn update(&self, address: &CachedAddress) -> Result<(), Self::Error>; /// TODO: Maybe turn this into another db /// Returns the height of the last block we filtered fn get_cache_height(&self) -> Result; @@ -185,7 +199,7 @@ pub trait AddressCacheDatabase { /// Get associated descriptors fn descs_get(&self) -> Result, Self::Error>; /// Get a transaction from the database - fn get_transaction(&self, txid: &Txid) -> Result; + fn get_transaction(&self, txid: &Txid) -> Result, Self::Error>; /// Saves a transaction to the database fn save_transaction(&self, tx: &CachedTransaction) -> Result<(), Self::Error>; /// Returns all transaction we have cached so far @@ -208,7 +222,11 @@ struct AddressCacheInner { impl AddressCacheInner { /// Iterates through a block, finds transactions destined to ourselves. /// Returns all transactions we found. - fn block_process(&mut self, block: &Block, height: u32) -> Vec<(Transaction, TxOut)> { + fn block_process( + &mut self, + block: &Block, + height: u32, + ) -> Result, WatchOnlyError> { let mut my_transactions = Vec::new(); // Check if this transaction spends from one of our utxos for (position, transaction) in block.txdata.iter().enumerate() { @@ -217,17 +235,18 @@ impl AddressCacheInner { let script = self .address_map .get(script) - .expect("Can't cache a utxo for a address we don't have") + .ok_or(WatchOnlyError::CachedAddressNotFound)? .to_owned(); + let tx = self - .get_transaction(&txin.previous_output.txid) - .expect("We cached a utxo for a transaction we don't have"); + .get_transaction(&txin.previous_output.txid)? + .ok_or(WatchOnlyError::TransactionNotFound)?; let utxo = tx .tx .output .get(txin.previous_output.vout as usize) - .expect("Did we cache an invalid utxo?"); + .ok_or(WatchOnlyError::IndexOutOfBounds)?; let merkle_block = MerkleProof::from_block(block, position as u64); @@ -240,7 +259,7 @@ impl AddressCacheInner { vin, true, script.script_hash, - ) + )?; } } // Checks if one of our addresses is the recipient of this transaction @@ -260,19 +279,17 @@ impl AddressCacheInner { vout, false, hash, - ); + )?; } } } - my_transactions + Ok(my_transactions) } - fn new(database: D) -> AddressCacheInner { - let scripts = database.load().expect("Could not load database"); + fn new(database: D) -> Result, WatchOnlyError> { + let scripts = database.load()?; if database.get_stats().is_err() { - database - .save_stats(&Stats::default()) - .expect("Could not save stats"); + database.save_stats(&Stats::default())?; } let mut address_map = HashMap::new(); let mut script_set = HashSet::new(); @@ -285,46 +302,68 @@ impl AddressCacheInner { address_map.insert(address.script_hash, address); } - AddressCacheInner { + Ok(AddressCacheInner { database, address_map, script_set, utxo_index, - } + }) } - fn get_address_utxos(&self, script_hash: &Hash) -> Option> { - let address = self.address_map.get(script_hash)?; + fn get_address_utxos( + &self, + script_hash: &Hash, + ) -> Result, WatchOnlyError> { + let address = self + .address_map + .get(script_hash) + .ok_or(WatchOnlyError::CachedAddressNotFound)?; let utxos = &address.utxos; let mut address_utxos = Vec::new(); for utxo in utxos { - let tx = self.get_transaction(&utxo.txid)?; - let txout = tx.tx.output.get(utxo.vout as usize)?; + let tx = self + .get_transaction(&utxo.txid)? + .ok_or(WatchOnlyError::TransactionNotFound)?; + let txout = tx + .tx + .output + .get(utxo.vout as usize) + .ok_or(WatchOnlyError::IndexOutOfBounds)?; address_utxos.push((txout.clone(), *utxo)); } - Some(address_utxos) + Ok(address_utxos) } - fn get_transaction(&self, txid: &Txid) -> Option { - self.database.get_transaction(txid).ok() + fn get_transaction( + &self, + txid: &Txid, + ) -> Result, WatchOnlyError> { + Ok(self.database.get_transaction(txid)?) } /// Returns all transactions this address has, both input and outputs - fn get_address_history(&self, script_hash: &Hash) -> Option> { - let cached_script = self.address_map.get(script_hash)?; - let mut transactions: Vec<_> = cached_script - .transactions - .iter() - .filter_map(|txid| self.get_transaction(txid)) - .collect(); + fn get_address_history( + &self, + script_hash: &Hash, + ) -> Result, WatchOnlyError> { + let cached_script = self + .address_map + .get(script_hash) + .ok_or(WatchOnlyError::CachedAddressNotFound)?; + let mut transactions = Vec::new(); + for txid in &cached_script.transactions { + if let Some(tx) = self.get_transaction(txid)? { + transactions.push(tx); + } + } let mut unconfirmed = transactions.clone(); transactions.retain(|tx| tx.height != 0); transactions.sort(); unconfirmed.retain(|tx| tx.height == 0); transactions.extend(unconfirmed); - Some(transactions) + Ok(transactions) } /// Get [Merkle Proof] @@ -332,18 +371,21 @@ impl AddressCacheInner { /// Returns none if a given Txid is an unconfirmed transaction or unrelated with your wallet, defined by the xpubs, descriptors and addresses in your `config.toml`. /// /// [Merkle Proof]: https://developer.bitcoin.org/devguide/block_chain.html#merkle-trees - fn get_merkle_proof(&self, txid: &Txid) -> Option { + fn get_merkle_proof( + &self, + txid: &Txid, + ) -> Result, WatchOnlyError> { // If a given transaction is cached, but the merkle tree doesn't exist, that means // it is an unconfirmed transaction. - self.get_transaction(txid)?.clone().merkle_block + Ok(self.get_transaction(txid)?.and_then(|tx| tx.merkle_block)) } /// Adds a new address to track, should be called at wallet setup and every once in a while /// to cache new addresses, as we use the first ones. Only requires a script to cache. - fn cache_address(&mut self, script_pk: ScriptBuf) { + fn cache_address(&mut self, script_pk: ScriptBuf) -> Result<(), WatchOnlyError> { let hash = get_spk_hash(&script_pk); if self.address_map.contains_key(&hash) { - return; + return Ok(()); } let new_address = CachedAddress { balance: 0, @@ -352,10 +394,11 @@ impl AddressCacheInner { transactions: Vec::new(), utxos: Vec::new(), }; - self.database.save(&new_address); + self.database.save(&new_address)?; self.address_map.insert(hash, new_address); self.script_set.insert(hash); + Ok(()) } /// Setup is the first command that should be executed. In a new cache. It sets our wallet's @@ -378,22 +421,20 @@ impl AddressCacheInner { ) .map_err(WatchOnlyError::InvalidDescriptor)?; - addresses.iter().for_each(|address| { - self.cache_address(address.clone()); - }); + for address in addresses { + self.cache_address(address)?; + } stats.derivation_index += DERIVATION_COUNT; Ok(self.database.save_stats(&stats)?) } - fn maybe_derive_addresses(&mut self) { - let stats = self.database.get_stats().unwrap(); + fn maybe_derive_addresses(&mut self) -> Result<(), WatchOnlyError> { + let stats = self.database.get_stats()?; if stats.transaction_count > (stats.derivation_index as usize * DERIVATION_COUNT as usize) { - let res = self.derive_addresses(); - if res.is_err() { - error!("Error deriving addresses: {res:?}"); - } + self.derive_addresses()?; } + Ok(()) } fn find_unconfirmed(&self) -> Result, WatchOnlyError> { @@ -401,7 +442,10 @@ impl AddressCacheInner { let mut unconfirmed = Vec::new(); for tx in transactions { - let tx = self.database.get_transaction(&tx)?; + let tx = self + .database + .get_transaction(&tx)? + .ok_or(WatchOnlyError::TransactionNotFound)?; if tx.height == 0 { unconfirmed.push(tx.tx); } @@ -409,27 +453,35 @@ impl AddressCacheInner { Ok(unconfirmed) } - fn find_spend(&self, transaction: &Transaction) -> Vec<(usize, TxOut)> { + fn find_spend( + &self, + transaction: &Transaction, + ) -> Result, WatchOnlyError> { let mut spends = Vec::new(); for (idx, input) in transaction.input.iter().enumerate() { if self.utxo_index.contains_key(&input.previous_output) { - let prev_tx = self.get_transaction(&input.previous_output.txid).unwrap(); + let prev_tx = self + .get_transaction(&input.previous_output.txid)? + .ok_or(WatchOnlyError::TransactionNotFound)?; spends.push(( idx, prev_tx.tx.output[input.previous_output.vout as usize].clone(), )); } } - spends + Ok(spends) } - fn cache_mempool_transaction(&mut self, transaction: &Transaction) -> Vec { - let mut coins = self.find_spend(transaction); + fn cache_mempool_transaction( + &mut self, + transaction: &Transaction, + ) -> Result, WatchOnlyError> { + let mut coins = self.find_spend(transaction)?; for (idx, spend) in coins.iter() { let script = self .address_map .get(&get_spk_hash(&spend.script_pubkey)) - .unwrap() + .ok_or(WatchOnlyError::CachedAddressNotFound)? .to_owned(); self.cache_transaction( transaction, @@ -440,12 +492,16 @@ impl AddressCacheInner { *idx, true, script.script_hash, - ) + )?; } for (idx, out) in transaction.output.iter().enumerate() { let spk_hash = get_spk_hash(&out.script_pubkey); if self.script_set.contains(&spk_hash) { - let script = self.address_map.get(&spk_hash).unwrap().to_owned(); + let script = self + .address_map + .get(&spk_hash) + .ok_or(WatchOnlyError::CachedAddressNotFound)? + .to_owned(); coins.push((idx, out.clone())); self.cache_transaction( transaction, @@ -456,23 +512,28 @@ impl AddressCacheInner { idx, true, script.script_hash, - ) + )?; } } - coins + Ok(coins .iter() .cloned() .unzip::, Vec>() - .1 + .1) } - fn save_mempool_tx(&mut self, hash: Hash, transaction_to_cache: CachedTransaction) { + fn save_mempool_tx( + &mut self, + hash: Hash, + transaction_to_cache: CachedTransaction, + ) -> Result<(), WatchOnlyError> { if let Some(address) = self.address_map.get_mut(&hash) { if !address.transactions.contains(&transaction_to_cache.hash) { address.transactions.push(transaction_to_cache.hash); - self.database.update(address); + self.database.update(address)?; } } + Ok(()) } fn save_non_mempool_tx( @@ -483,7 +544,7 @@ impl AddressCacheInner { index: usize, hash: Hash, transaction_to_cache: CachedTransaction, - ) { + ) -> Result<(), WatchOnlyError> { if let Some(address) = self.address_map.get_mut(&hash) { // This transaction is spending from this address, so we should remove the UTXO if is_spend { @@ -492,7 +553,7 @@ impl AddressCacheInner { let input = transaction .input .get(index) - .expect("Malformed call, index is bigger than the output vector"); + .ok_or(WatchOnlyError::IndexOutOfBounds)?; let idx = address .utxos .iter() @@ -514,9 +575,10 @@ impl AddressCacheInner { if !address.transactions.contains(&transaction_to_cache.hash) { address.transactions.push(transaction_to_cache.hash); - self.database.update(address); + self.database.update(address)?; } } + Ok(()) } /// Caches a new transaction. This method may be called for addresses we don't follow yet, @@ -532,7 +594,7 @@ impl AddressCacheInner { index: usize, is_spend: bool, hash: sha256::Hash, - ) { + ) -> Result<(), WatchOnlyError> { let transaction_to_cache = CachedTransaction { height, merkle_block: Some(merkle_block), @@ -540,9 +602,7 @@ impl AddressCacheInner { hash: transaction.compute_txid(), position, }; - self.database - .save_transaction(&transaction_to_cache) - .expect("Database not working"); + self.database.save_transaction(&transaction_to_cache)?; if let Entry::Vacant(e) = self.address_map.entry(hash) { let script = transaction.output[index].script_pubkey.clone(); @@ -557,12 +617,12 @@ impl AddressCacheInner { transactions: Vec::new(), utxos: Vec::new(), }; - self.database.save(&new_address); + self.database.save(&new_address)?; e.insert(new_address); self.script_set.insert(hash); } - self.maybe_derive_addresses(); + self.maybe_derive_addresses()?; // Confirmed transaction if height > 0 { return self.save_non_mempool_tx( @@ -575,7 +635,7 @@ impl AddressCacheInner { ); } // Unconfirmed transaction - self.save_mempool_tx(hash, transaction_to_cache); + self.save_mempool_tx(hash, transaction_to_cache) } } @@ -596,71 +656,93 @@ impl BlockConsumer for AddressC height: u32, _spent_utxos: Option<&HashMap>, ) { - self.block_process(block, height); + if let Err(e) = self.block_process(block, height) { + error!("Error processing block at height {height}: {e}"); + } } } impl AddressCache { - pub fn new(database: D) -> AddressCache { - AddressCache { - inner: RwLock::new(AddressCacheInner::new(database)), - } + pub fn new(database: D) -> Result, WatchOnlyError> { + Ok(AddressCache { + inner: RwLock::new(AddressCacheInner::new(database)?), + }) } - pub fn get_utxo(&self, outpoint: &OutPoint) -> Option { - let inner = self.inner.read().expect("poisoned lock"); - // a dirty way to check if the utxo is still unspent - let _ = inner.utxo_index.get(outpoint)?; - let tx = inner.get_transaction(&outpoint.txid)?; + fn read_inner( + &self, + ) -> Result>, WatchOnlyError> { + self.inner.read().map_err(|_| WatchOnlyError::PoisonedLock) + } + + fn write_inner( + &self, + ) -> Result>, WatchOnlyError> { + self.inner.write().map_err(|_| WatchOnlyError::PoisonedLock) + } - Some(tx.tx.output[outpoint.vout as usize].clone()) + pub fn get_utxo(&self, outpoint: &OutPoint) -> Result> { + let inner = self.read_inner()?; + // a dirty way to check if the utxo is still unspent + inner + .utxo_index + .get(outpoint) + .ok_or(WatchOnlyError::CachedAddressNotFound)?; + let tx = inner + .get_transaction(&outpoint.txid)? + .ok_or(WatchOnlyError::TransactionNotFound)?; + + tx.tx + .output + .get(outpoint.vout as usize) + .cloned() + .ok_or(WatchOnlyError::IndexOutOfBounds) } - pub fn n_cached_addresses(&self) -> usize { - let inner = self.inner.read().expect("poisoned lock"); - inner.address_map.len() + pub fn n_cached_addresses(&self) -> Result> { + let inner = self.read_inner()?; + Ok(inner.address_map.len()) } /// Returns the balance of this address, debts (spends) are taken in account - pub fn get_address_balance(&self, script_hash: &Hash) -> Option { - let inner = self.inner.read().expect("poisoned lock"); - - Some(inner.address_map.get(script_hash)?.balance) + pub fn get_address_balance( + &self, + script_hash: &Hash, + ) -> Result, WatchOnlyError> { + let inner = self.read_inner()?; + Ok(inner.address_map.get(script_hash).map(|a| a.balance)) } - pub fn get_cached_addresses(&self) -> Vec { - let inner = self.inner.read().expect("poisoned lock"); - inner + pub fn get_cached_addresses(&self) -> Result, WatchOnlyError> { + let inner = self.read_inner()?; + Ok(inner .address_map .values() .map(|address| address.script.clone()) - .collect() + .collect()) } - pub fn bump_height(&self, height: u32) { - let inner = self.inner.read().expect("poisoned lock"); - inner - .database - .set_cache_height(height) - .expect("Database is not working"); + pub fn bump_height(&self, height: u32) -> Result<(), WatchOnlyError> { + let inner = self.read_inner()?; + Ok(inner.database.set_cache_height(height)?) } - pub fn get_cache_height(&self) -> u32 { - let inner = self.inner.read().expect("poisoned lock"); - inner.database.get_cache_height().unwrap_or(0) + pub fn get_cache_height(&self) -> Result> { + let inner = self.read_inner()?; + Ok(inner.database.get_cache_height()?) } /// Tells whether or not a descriptor is already cached pub fn is_cached(&self, desc: &str) -> Result> { - let inner = self.inner.read().expect("poisoned lock"); + let inner = self.read_inner()?; let known_descs = inner.database.descs_get()?; Ok(known_descs.iter().any(|s| s == desc)) } /// Tells whether an address is already cached - pub fn is_address_cached(&self, script_hash: &Hash) -> bool { - let inner = self.inner.read().expect("poisoned lock"); - inner.address_map.contains_key(script_hash) + pub fn is_address_cached(&self, script_hash: &Hash) -> Result> { + let inner = self.read_inner()?; + Ok(inner.address_map.contains_key(script_hash)) } /// Push a descriptor into the wallet checking whether it is already cached, returning an error if so @@ -677,10 +759,10 @@ impl AddressCache { .map_err(WatchOnlyError::InvalidDescriptor)?; for address in address_descriptors.clone() { - self.cache_address(address); + self.cache_address(address)?; } - let inner = self.inner.write().expect("poisoned lock"); + let inner = self.write_inner()?; inner.database.desc_save(descriptor)?; Ok(address_descriptors) @@ -698,90 +780,115 @@ impl AddressCache { Ok(()) } - pub fn get_position(&self, txid: &Txid) -> Option { - let inner = self.inner.read().expect("poisoned lock"); - Some(inner.get_transaction(txid)?.position) + pub fn get_position(&self, txid: &Txid) -> Result, WatchOnlyError> { + let inner = self.read_inner()?; + Ok(inner.get_transaction(txid)?.map(|tx| tx.position)) } - pub fn get_height(&self, txid: &Txid) -> Option { - let inner = self.inner.read().expect("poisoned lock"); - Some(inner.get_transaction(txid)?.height) + pub fn get_height(&self, txid: &Txid) -> Result, WatchOnlyError> { + let inner = self.read_inner()?; + Ok(inner.get_transaction(txid)?.map(|tx| tx.height)) } - pub fn get_cached_transaction(&self, txid: &Txid) -> Option { - let inner = self.inner.read().expect("poisoned lock"); - let tx = inner.get_transaction(txid)?; - Some(serialize_hex(&tx.tx)) + pub fn get_cached_transaction( + &self, + txid: &Txid, + ) -> Result, WatchOnlyError> { + let inner = self.read_inner()?; + Ok(inner.get_transaction(txid)?.map(|tx| serialize_hex(&tx.tx))) } pub fn setup(&self) -> Result<(), WatchOnlyError> { - let inner = self.inner.read().expect("poisoned lock"); + let inner = self.read_inner()?; inner.setup() } - pub fn block_process(&self, block: &Block, height: u32) -> Vec<(Transaction, TxOut)> { - let mut inner = self.inner.write().expect("poisoned lock"); + pub fn block_process( + &self, + block: &Block, + height: u32, + ) -> Result, WatchOnlyError> { + let mut inner = self.write_inner()?; inner.block_process(block, height) } - pub fn get_address_utxos(&self, script_hash: &Hash) -> Option> { - let inner = self.inner.read().expect("poisoned lock"); + pub fn get_address_utxos( + &self, + script_hash: &Hash, + ) -> Result, WatchOnlyError> { + let inner = self.read_inner()?; inner.get_address_utxos(script_hash) } - pub fn get_transaction(&self, txid: &Txid) -> Option { - let inner = self.inner.read().expect("poisoned lock"); + pub fn get_transaction( + &self, + txid: &Txid, + ) -> Result, WatchOnlyError> { + let inner = self.read_inner()?; inner.get_transaction(txid) } - pub fn get_address_history(&self, script_hash: &Hash) -> Option> { - let inner = self.inner.read().expect("poisoned lock"); + pub fn get_address_history( + &self, + script_hash: &Hash, + ) -> Result, WatchOnlyError> { + let inner = self.read_inner()?; inner.get_address_history(script_hash) } /// Returns the Merkle Proof for a given txid. /// /// Fails if a given Txid is an unconfirmed transaction. - pub fn get_merkle_proof(&self, txid: &Txid) -> Option { - let inner = self.inner.read().expect("poisoned lock"); + pub fn get_merkle_proof( + &self, + txid: &Txid, + ) -> Result, WatchOnlyError> { + let inner = self.read_inner()?; inner.get_merkle_proof(txid) } pub fn derive_addresses(&self) -> Result<(), WatchOnlyError> { - let mut inner = self.inner.write().expect("poisoned lock"); + let mut inner = self.write_inner()?; inner.derive_addresses() } pub fn get_stats(&self) -> Result> { - let inner = self.inner.read().expect("poisoned lock"); + let inner = self.read_inner()?; inner .database .get_stats() .map_err(WatchOnlyError::DatabaseError) } - pub fn maybe_derive_addresses(&self) { - let mut inner = self.inner.write().expect("poisoned lock"); + pub fn maybe_derive_addresses(&self) -> Result<(), WatchOnlyError> { + let mut inner = self.write_inner()?; inner.maybe_derive_addresses() } pub fn find_unconfirmed(&self) -> Result, WatchOnlyError> { - let inner = self.inner.read().expect("poisoned lock"); + let inner = self.read_inner()?; inner.find_unconfirmed() } - pub fn cache_address(&self, script_pk: ScriptBuf) { - let mut inner = self.inner.write().expect("poisoned lock"); + pub fn cache_address(&self, script_pk: ScriptBuf) -> Result<(), WatchOnlyError> { + let mut inner = self.write_inner()?; inner.cache_address(script_pk) } - pub fn cache_mempool_transaction(&self, transaction: &Transaction) -> Vec { - let mut inner = self.inner.write().expect("poisoned lock"); + pub fn cache_mempool_transaction( + &self, + transaction: &Transaction, + ) -> Result, WatchOnlyError> { + let mut inner = self.write_inner()?; inner.cache_mempool_transaction(transaction) } - pub fn save_mempool_tx(&self, hash: Hash, transaction_to_cache: CachedTransaction) { - let mut inner = self.inner.write().expect("poisoned lock"); + pub fn save_mempool_tx( + &self, + hash: Hash, + transaction_to_cache: CachedTransaction, + ) -> Result<(), WatchOnlyError> { + let mut inner = self.write_inner()?; inner.save_mempool_tx(hash, transaction_to_cache) } @@ -793,8 +900,8 @@ impl AddressCache { index: usize, hash: Hash, transaction_to_cache: CachedTransaction, - ) { - let mut inner = self.inner.write().expect("poisoned lock"); + ) -> Result<(), WatchOnlyError> { + let mut inner = self.write_inner()?; inner.save_non_mempool_tx( transaction, is_spend, @@ -806,7 +913,7 @@ impl AddressCache { } pub fn get_descriptors(&self) -> Result, WatchOnlyError> { - let inner = self.inner.read().expect("poisoned lock"); + let inner = self.read_inner()?; inner .database .descs_get() @@ -824,8 +931,8 @@ impl AddressCache { index: usize, is_spend: bool, hash: sha256::Hash, - ) { - let mut inner = self.inner.write().expect("poisoned lock"); + ) -> Result<(), WatchOnlyError> { + let mut inner = self.write_inner()?; inner.cache_transaction( transaction, height, @@ -854,9 +961,13 @@ mod test { use bitcoin::Txid; use floresta_common::get_spk_hash; use floresta_common::prelude::*; + use kv::Config; + use kv::Store; use super::memory_database::MemoryDatabase; use super::AddressCache; + use super::WatchOnlyError; + use crate::kv_database::KvDatabase; use crate::merkle::MerkleProof; use crate::DERIVATION_COUNT; @@ -870,7 +981,7 @@ mod test { fn get_test_cache() -> AddressCache { let database = MemoryDatabase::new(); - AddressCache::new(database) + AddressCache::new(database).expect("MemoryDatabase::new should never fail") } fn get_test_address() -> (Address, sha256::Hash) { @@ -881,6 +992,25 @@ mod test { (address, script_hash) } + #[test] + fn new_returns_err_when_database_load_fails() { + let test_id = rand::random::(); + let path = format!("./tmp-db/test-corrupt-{test_id}/"); + { + let cfg = Config::new(&path); + let store = Store::new(cfg).unwrap(); + let bucket = store.bucket::>(Some("addresses")).unwrap(); + bucket + .set(&"corrupted_entry".to_string(), &vec![0xFF, 0xFE]) + .unwrap(); + bucket.flush().unwrap(); + } + let db = KvDatabase::new(path.clone()).unwrap(); + let result = AddressCache::new(db); + let _ = std::fs::remove_dir_all(&path); + assert!(matches!(result, Err(WatchOnlyError::DatabaseError(_)))); + } + #[test] fn test_create() { let _ = get_test_cache(); @@ -891,13 +1021,13 @@ mod test { let (address, script_hash) = get_test_address(); let cache = get_test_cache(); // Should have no address before caching - assert_eq!(cache.n_cached_addresses(), 0); + assert_eq!(cache.n_cached_addresses().unwrap(), 0); - cache.cache_address(address.script_pubkey()); + cache.cache_address(address.script_pubkey()).unwrap(); // Assert we indeed have one cached address - assert_eq!(cache.n_cached_addresses(), 1); - assert_eq!(cache.get_address_balance(&script_hash), Some(0)); - assert_eq!(cache.get_address_history(&script_hash), Some(Vec::new())); + assert_eq!(cache.n_cached_addresses().unwrap(), 1); + assert_eq!(cache.get_address_balance(&script_hash).unwrap(), Some(0)); + assert_eq!(cache.get_address_history(&script_hash).unwrap(), Vec::new()); } #[test] @@ -915,25 +1045,30 @@ mod test { let (_, script_hash) = get_test_address(); let cache = get_test_cache(); - cache.cache_transaction( - &transaction, - 118511, - transaction.output[0].value.to_sat(), - merkle_block, - 1, - 0, - false, - get_spk_hash(&transaction.output[0].script_pubkey), - ); + cache + .cache_transaction( + &transaction, + 118511, + transaction.output[0].value.to_sat(), + merkle_block, + 1, + 0, + false, + get_spk_hash(&transaction.output[0].script_pubkey), + ) + .unwrap(); assert_eq!( script_hash, get_spk_hash(&transaction.output[0].script_pubkey) ); - let balance = cache.get_address_balance(&script_hash); + let balance = cache.get_address_balance(&script_hash).unwrap(); let history = cache.get_address_history(&script_hash).unwrap(); - let cached_merkle_block = cache.get_merkle_proof(&transaction.compute_txid()).unwrap(); + let cached_merkle_block = cache + .get_merkle_proof(&transaction.compute_txid()) + .unwrap() + .unwrap(); assert_eq!(balance, Some(999890)); assert_eq!( Ok(history[0].hash), @@ -949,17 +1084,21 @@ mod test { // TESTS FOR SMALL, HELPER FUNCTIONS // [get_position] - assert_eq!(cache.get_position(&transaction.compute_txid()).unwrap(), 1); + assert_eq!( + cache.get_position(&transaction.compute_txid()).unwrap(), + Some(1) + ); // [get_height] assert_eq!( cache.get_height(&transaction.compute_txid()).unwrap(), - 118511 + Some(118511) ); // [get_cached_transaction] assert!(cache .get_cached_transaction(&transaction.compute_txid()) + .unwrap() .is_some()); // [get_address_utxos] @@ -978,16 +1117,18 @@ mod test { let transaction = Vec::from_hex(transaction).unwrap(); let transaction = deserialize(&transaction).unwrap(); - cache.cache_transaction( - &transaction, - 0, - transaction.output[1].value.to_sat(), - MerkleProof::default(), - 2, - 1, - false, - get_spk_hash(&transaction.output[1].script_pubkey), - ); + cache + .cache_transaction( + &transaction, + 0, + transaction.output[1].value.to_sat(), + MerkleProof::default(), + 2, + 1, + false, + get_spk_hash(&transaction.output[1].script_pubkey), + ) + .unwrap(); assert_eq!( cache.find_unconfirmed().unwrap()[0].compute_txid(), @@ -999,18 +1140,18 @@ mod test { fn test_process_block() { let (address, script_hash) = get_test_address(); let cache = get_test_cache(); - cache.cache_address(address.script_pubkey()); + cache.cache_address(address.script_pubkey()).unwrap(); let block = "000000203ea734fa2c8dee7d3194878c9eaf6e83a629f79b3076ec857793995e01010000eb99c679c0305a1ac0f5eb2a07a9f080616105e605b92b8c06129a2451899225ab5481633c4b011e0b26720102020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0403efce01feffffff026ef2052a01000000225120a1a1b1376d5165617a50a6d2f59abc984ead8a92df2b25f94b53dbc2151824730000000000000000776a24aa21a9ed1b4c48a7220572ff3ab3d2d1c9231854cb62542fbb1e0a4b21ebbbcde8d652bc4c4fecc7daa2490047304402204b37c41fce11918df010cea4151737868111575df07f7f2945d372e32a6d11dd02201658873a8228d7982df6bdbfff5d0cad1d6f07ee400e2179e8eaad8d115b7ed001000120000000000000000000000000000000000000000000000000000000000000000000000000020000000001017ca523c5e6df0c014e837279ab49be1676a9fe7571c3989aeba1e5d534f4054a0000000000fdffffff01d2410f00000000001600142b6a2924aa9b1b115d1ac3098b0ba0e6ed510f2a02473044022071b8583ba1f10531b68cb5bd269fb0e75714c20c5a8bce49d8a2307d27a082df022069a978dac00dd9d5761aa48c7acc881617fa4d2573476b11685596b17d437595012103b193d06bd0533d053f959b50e3132861527e5a7a49ad59c5e80a265ff6a77605eece0100"; let block = deserialize(&Vec::from_hex(block).unwrap()).unwrap(); - cache.block_process(&block, 118511); + cache.block_process(&block, 118511).unwrap(); - let balance = cache.get_address_balance(&script_hash); + let balance = cache.get_address_balance(&script_hash).unwrap(); let history = cache.get_address_history(&script_hash).unwrap(); let transaction_id = Txid::from_str("6bb0665122c7dcecc6e6c45b6384ee2bdce148aea097896e6f3e9e08070353ea") .unwrap(); - let cached_merkle_block = cache.get_merkle_proof(&transaction_id).unwrap(); + let cached_merkle_block = cache.get_merkle_proof(&transaction_id).unwrap().unwrap(); assert_eq!(balance, Some(999890)); assert_eq!( history[0].hash, @@ -1027,8 +1168,8 @@ mod test { // TESTS FOR SMALL HELPER FUNCTIONS // [bump_height], [get_cache_height], [set_cache_height] - cache.bump_height(118511); - assert_eq!(cache.get_cache_height(), 118511); + cache.bump_height(118511).unwrap(); + assert_eq!(cache.get_cache_height().unwrap(), 118511); // [is_cached], [push_descriptor] let desc = "wsh(sortedmulti(1,[54ff5a12/48h/1h/0h/2h]tpubDDw6pwZA3hYxcSN32q7a5ynsKmWr4BbkBNHydHPKkM4BZwUfiK7tQ26h7USm8kA1E2FvCy7f7Er7QXKF8RNptATywydARtzgrxuPDwyYv4x/<0;1>/*,[bcf969c0/48h/1h/0h/2h]tpubDEFdgZdCPgQBTNtGj4h6AehK79Jm4LH54JrYBJjAtHMLEAth7LuY87awx9ZMiCURFzFWhxToRJK6xp39aqeJWrG5nuW3eBnXeMJcvDeDxfp/<0;1>/*))#fuw35j0q"; @@ -1053,10 +1194,10 @@ mod test { let script_hash = get_spk_hash(&spk); let cache = get_test_cache(); - cache.cache_address(spk); + cache.cache_address(spk).unwrap(); - cache.block_process(&block1, 118511); - cache.block_process(&block2, 118509); + cache.block_process(&block1, 118511).unwrap(); + cache.block_process(&block2, 118509).unwrap(); let address = cache.inner.read().unwrap(); let address = address.address_map.get(&script_hash).unwrap(); diff --git a/crates/floresta-watch-only/src/memory_database.rs b/crates/floresta-watch-only/src/memory_database.rs index 5b4e9acbc..633ae1847 100644 --- a/crates/floresta-watch-only/src/memory_database.rs +++ b/crates/floresta-watch-only/src/memory_database.rs @@ -53,14 +53,12 @@ impl MemoryDatabase { } impl AddressCacheDatabase for MemoryDatabase { type Error = MemoryDatabaseError; - fn save(&self, address: &CachedAddress) { - self.get_inner_mut() - .map(|mut inner| { - inner - .addresses - .insert(address.script_hash, address.to_owned()) - }) - .unwrap(); + fn save(&self, address: &CachedAddress) -> Result<()> { + self.get_inner_mut().map(|mut inner| { + inner + .addresses + .insert(address.script_hash, address.to_owned()); + }) } fn load(&self) -> Result> { @@ -78,15 +76,13 @@ impl AddressCacheDatabase for MemoryDatabase { Ok(()) } - fn update(&self, address: &super::CachedAddress) { - self.get_inner_mut() - .map(|mut inner| { - inner - .addresses - .entry(address.script_hash) - .and_modify(|addr| addr.clone_from(address)); - }) - .unwrap(); + fn update(&self, address: &super::CachedAddress) -> Result<()> { + self.get_inner_mut().map(|mut inner| { + inner + .addresses + .entry(address.script_hash) + .and_modify(|addr| addr.clone_from(address)); + }) } fn get_cache_height(&self) -> Result { @@ -108,11 +104,8 @@ impl AddressCacheDatabase for MemoryDatabase { Ok(self.get_inner()?.descriptors.to_owned()) } - fn get_transaction(&self, txid: &bitcoin::Txid) -> Result { - if let Some(tx) = self.get_inner()?.transactions.get(txid) { - return Ok(tx.clone()); - } - Err(MemoryDatabaseError::PoisonedLock) + fn get_transaction(&self, txid: &bitcoin::Txid) -> Result> { + Ok(self.get_inner()?.transactions.get(txid).cloned()) } fn save_transaction(&self, tx: &super::CachedTransaction) -> Result<()> { diff --git a/crates/floresta/examples/watch-only.rs b/crates/floresta/examples/watch-only.rs index 3ec6c24ae..649237602 100644 --- a/crates/floresta/examples/watch-only.rs +++ b/crates/floresta/examples/watch-only.rs @@ -17,7 +17,8 @@ fn main() { // the `AddressCacheDatabase` trait. let wallet_data = MemoryDatabase::new(); // Then, we create the wallet itself. - let wallet = AddressCache::new(wallet_data); + let wallet = AddressCache::new(wallet_data) + .expect("AddressCache::new should never fail with a MemoryDatabase"); // Now, we need to add the addresses we want to watch. We can add them one by one, or // we can add a descriptor that will generate the addresses for us. Here, we use a // descriptor that generates P2WPKH addresses. The descriptor is parsed using the @@ -36,21 +37,25 @@ fn main() { // We can now add the descriptor to the wallet. This will generate the first 100 addresses // for us, and add them to the wallet. for i in 0..100 { - wallet.cache_address(bitcoin::ScriptBuf::from( - descriptor - .at_derivation_index(i) - .unwrap() - .explicit_script() - .unwrap() - .as_bytes() - .to_vec(), - )); + wallet + .cache_address(bitcoin::ScriptBuf::from( + descriptor + .at_derivation_index(i) + .unwrap() + .explicit_script() + .unwrap() + .as_bytes() + .to_vec(), + )) + .unwrap(); } // We can now process some blocks. Here, we process the first 11 blocks of a custom // regtest network. Each coinbase some of the addresses derived above. for block in BLOCKS.iter() { let block = Vec::from_hex(block).unwrap(); - let _ = wallet.block_process(&deserialize(&block).unwrap(), 1); + wallet + .block_process(&deserialize(&block).unwrap(), 1) + .unwrap(); } // We can now query the wallet for information about the addresses we added. For example, // we can get the history of the second address, the balance, and the UTXOs. To fetch the @@ -69,9 +74,9 @@ fn main() { .map(|tx| tx.hash) .collect::>(); // Fetch the balance of the address. - let balance = wallet.get_address_balance(&hash); + let balance = wallet.get_address_balance(&hash).ok().flatten(); // Fetch the UTXOs of the address. - let utxos = wallet.get_address_utxos(&hash); + let utxos = wallet.get_address_utxos(&hash).ok(); // We can now print the information we fetched. println!("************** Wallet Summary *****************\n");