Skip to content
33 changes: 28 additions & 5 deletions src/database/identities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -148,7 +153,8 @@ impl Database {
let wallet_index: Option<u32> = row.get(2)?;
let status: Option<u8> = row.get(3)?;

let mut identity = QualifiedIdentity::from_bytes(&data);
let mut identity =
QualifiedIdentity::from_bytes(&data).map_err(super::CorruptedBlobError)?;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
identity.alias = alias;
identity.wallet_index = wallet_index;
identity.status = IdentityStatus::from_u8(status.unwrap_or(2));
Expand All @@ -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,
Expand Down Expand Up @@ -207,7 +216,8 @@ impl Database {
let wallet_index: Option<u32> = row.get(2)?;
let status: Option<u8> = 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));
Expand All @@ -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,
Expand Down Expand Up @@ -261,7 +274,8 @@ impl Database {
let wallet_index: Option<u32> = row.get(2)?;
let status: Option<u8> = 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));
Expand All @@ -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,
Expand All @@ -297,7 +314,8 @@ impl Database {
)?;
let identity_iter = stmt.query_map(params![network], |row| {
let data: Vec<u8> = row.get(0)?;
let mut identity: QualifiedIdentity = QualifiedIdentity::from_bytes(&data);
let mut identity =
QualifiedIdentity::from_bytes(&data).map_err(super::CorruptedBlobError)?;
Comment thread
lklimek marked this conversation as resolved.
identity.network = app_context.network;

Ok(identity)
Expand All @@ -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(
Expand All @@ -325,7 +347,8 @@ impl Database {
stmt.query_map(params![network], |row| {
let data: Vec<u8> = row.get(0)?;
let wallet_id: Option<WalletSeedHash> = 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))
Expand Down
19 changes: 19 additions & 0 deletions src/database/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<CorruptedBlobError> for rusqlite::Error {
fn from(e: CorruptedBlobError) -> Self {
rusqlite::Error::FromSqlConversionFailure(0, rusqlite::types::Type::Blob, Box::new(e))
}
}
Comment thread
lklimek marked this conversation as resolved.

#[derive(Debug)]
pub struct Database {
conn: Mutex<Connection>,
Expand Down
8 changes: 7 additions & 1 deletion src/database/scheduled_votes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
20 changes: 18 additions & 2 deletions src/database/wallet.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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<Vec<Wallet>> {
let network_str = network.to_string();
let conn = self.conn.lock().unwrap();
Expand Down Expand Up @@ -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;

Expand Down
14 changes: 10 additions & 4 deletions src/model/qualified_identity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self, String> {
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 {
Expand Down
Loading