diff --git a/src/database/identities.rs b/src/database/identities.rs index b656c9291..6cb2a25fe 100644 --- a/src/database/identities.rs +++ b/src/database/identities.rs @@ -114,6 +114,11 @@ impl Database { Ok(()) } + /// Returns all local identities for the current network. + /// + /// Stops on the first corrupted identity blob and returns an error. + /// This is intentional — identities hold private keys and balance data, + /// so skipping a corrupted entry could cause loss of funds. pub fn get_local_qualified_identities( &self, app_context: &AppContext, @@ -148,7 +153,8 @@ impl Database { let wallet_index: Option = row.get(2)?; let status: Option = row.get(3)?; - let mut identity = QualifiedIdentity::from_bytes(&data); + let mut identity = + QualifiedIdentity::from_bytes(&data).map_err(super::CorruptedBlobError)?; identity.alias = alias; identity.wallet_index = wallet_index; identity.status = IdentityStatus::from_u8(status.unwrap_or(2)); @@ -173,6 +179,9 @@ impl Database { Ok(identities) } + /// Stops on the first corrupted identity blob and returns an error. + /// This is intentional — identities hold private keys and balance data, + /// so skipping a corrupted entry could cause loss of funds. #[allow(dead_code)] // May be used for filtering identities that belong to specific wallets pub fn get_local_qualified_identities_in_wallets( &self, @@ -207,7 +216,8 @@ impl Database { let wallet_index: Option = row.get(2)?; let status: Option = row.get(3)?; - let mut identity = QualifiedIdentity::from_bytes(&data); + let mut identity = + QualifiedIdentity::from_bytes(&data).map_err(super::CorruptedBlobError)?; identity.alias = alias; identity.wallet_index = wallet_index; identity.status = IdentityStatus::from_u8(status.unwrap_or(2)); @@ -232,6 +242,9 @@ impl Database { Ok(identities) } + /// Returns an error if the stored identity blob is corrupted. + /// This is intentional — identities hold private keys and balance data, + /// so ignoring corruption could cause loss of funds. pub fn get_identity_by_id( &self, identifier: &Identifier, @@ -261,7 +274,8 @@ impl Database { let wallet_index: Option = row.get(2)?; let status: Option = row.get(3)?; - let mut qi = QualifiedIdentity::from_bytes(&data); + let mut qi = + QualifiedIdentity::from_bytes(&data).map_err(super::CorruptedBlobError)?; qi.alias = alias; qi.wallet_index = wallet_index; qi.status = IdentityStatus::from_u8(status.unwrap_or(2)); @@ -285,6 +299,9 @@ impl Database { Ok(identity) } + /// Stops on the first corrupted identity blob and returns an error. + /// This is intentional — identities hold private keys and balance data, + /// so skipping a corrupted entry could cause loss of funds. pub fn get_local_voting_identities( &self, app_context: &AppContext, @@ -297,7 +314,8 @@ impl Database { )?; let identity_iter = stmt.query_map(params![network], |row| { let data: Vec = row.get(0)?; - let mut identity: QualifiedIdentity = QualifiedIdentity::from_bytes(&data); + let mut identity = + QualifiedIdentity::from_bytes(&data).map_err(super::CorruptedBlobError)?; identity.network = app_context.network; Ok(identity) @@ -309,6 +327,10 @@ impl Database { /// Retrieves all local user identities along with their associated wallet IDs. /// + /// Stops on the first corrupted identity blob and returns an error. + /// This is intentional — identities hold private keys and balance data, + /// so skipping a corrupted entry could cause loss of funds. + /// /// Caller should insert wallet references into associated_wallets before using the identities. #[allow(clippy::let_and_return)] pub fn get_local_user_identities( @@ -325,7 +347,8 @@ impl Database { stmt.query_map(params![network], |row| { let data: Vec = row.get(0)?; let wallet_id: Option = row.get(1)?; - let mut identity: QualifiedIdentity = QualifiedIdentity::from_bytes(&data); + let mut identity = + QualifiedIdentity::from_bytes(&data).map_err(super::CorruptedBlobError)?; identity.network = app_context.network; Ok((identity, wallet_id)) diff --git a/src/database/mod.rs b/src/database/mod.rs index c719d0bb7..758d204ce 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -20,6 +20,25 @@ use dash_sdk::dpp::dashcore::Network; use rusqlite::{Connection, Params}; use std::sync::Mutex; +/// Error indicating a corrupted data blob in the database. +/// +/// Converts into `rusqlite::Error::FromSqlConversionFailure` so it can +/// be propagated with `?` from any function returning `rusqlite::Result`. +/// +/// When a corrupted blob is encountered, processing stops immediately +/// (fail-fast) rather than skipping the row. This is intentional: identity +/// blobs contain private keys and balance data, so silently ignoring +/// corruption could result in loss of funds. +#[derive(Debug, thiserror::Error)] +#[error("corrupted data detected: {0}")] +pub(crate) struct CorruptedBlobError(pub String); + +impl From for rusqlite::Error { + fn from(e: CorruptedBlobError) -> Self { + rusqlite::Error::FromSqlConversionFailure(0, rusqlite::types::Type::Blob, Box::new(e)) + } +} + #[derive(Debug)] pub struct Database { conn: Mutex, diff --git a/src/database/scheduled_votes.rs b/src/database/scheduled_votes.rs index 9bf965f47..c70f0831f 100644 --- a/src/database/scheduled_votes.rs +++ b/src/database/scheduled_votes.rs @@ -159,7 +159,13 @@ impl Database { let executed_successfully: bool = match row.get(4)? { 0 => false, 1 => true, - _ => unreachable!(), + other => { + tracing::warn!( + "Unexpected value {} for executed column in scheduled_votes, defaulting to false", + other + ); + false + } }; let vote_choice = match vote_choice_string.as_str() { diff --git a/src/database/wallet.rs b/src/database/wallet.rs index 92226981d..77afe5f1d 100644 --- a/src/database/wallet.rs +++ b/src/database/wallet.rs @@ -1,4 +1,4 @@ -use crate::database::Database; +use crate::database::{CorruptedBlobError, Database}; use crate::model::qualified_identity::QualifiedIdentity; use crate::model::wallet::{ AddressInfo, ClosedKeyItem, DerivationPathReference, DerivationPathType, OpenWalletSeed, @@ -409,6 +409,11 @@ impl Database { } /// Retrieve all wallets for a specific network, including their addresses, balances, and known addresses. + /// + /// Stops on the first corrupted identity blob and returns an error for + /// the entire call. This is intentional — identities hold private keys + /// and balance data, so skipping a corrupted entry could cause loss of + /// funds. pub fn get_wallets(&self, network: &Network) -> rusqlite::Result> { let network_str = network.to_string(); let conn = self.conn.lock().unwrap(); @@ -814,7 +819,18 @@ impl Database { let (identity_data, wallet_seed_hash_array, wallet_index) = row?; if let Some(wallet) = wallets_map.get_mut(&wallet_seed_hash_array) { - let mut identity: QualifiedIdentity = QualifiedIdentity::from_bytes(&identity_data); + let mut identity = QualifiedIdentity::from_bytes(&identity_data).map_err(|e| { + tracing::warn!(wallet_index, error = %e, "found corrupted identity blob"); + rusqlite::Error::FromSqlConversionFailure( + 1, + rusqlite::types::Type::Blob, + CorruptedBlobError(format!( + "Failed to deserialize identity for wallet_index {}: {}", + wallet_index, e + )) + .into(), + ) + })?; identity.wallet_index = Some(wallet_index); identity.network = *network; diff --git a/src/model/qualified_identity/mod.rs b/src/model/qualified_identity/mod.rs index 510d10f0d..fbb913144 100644 --- a/src/model/qualified_identity/mod.rs +++ b/src/model/qualified_identity/mod.rs @@ -527,11 +527,17 @@ impl QualifiedIdentity { .expect("Failed to encode QualifiedIdentity") } - /// Deserializes a QualifiedIdentity from a vector of bytes. - pub fn from_bytes(bytes: &[u8]) -> Self { + /// Deserializes a `QualifiedIdentity` from a vector of bytes. + /// + /// Returns an error if the blob is corrupted or cannot be decoded. + /// Callers must stop processing on the first deserialization error rather + /// than skipping corrupted entries, because identities hold private keys + /// and balance information — silently ignoring a corrupted identity could + /// lead to loss of funds. + pub fn from_bytes(bytes: &[u8]) -> Result { bincode::decode_from_slice(bytes, bincode::config::standard()) - .expect("Failed to decode QualifiedIdentity") - .0 + .map(|(identity, _)| identity) + .map_err(|e| format!("Failed to decode QualifiedIdentity: {}", e)) } pub fn display_string(&self) -> String {