diff --git a/Cargo.lock b/Cargo.lock index d01ca90db8a..8035ecf34fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4881,6 +4881,7 @@ dependencies = [ "platform-encryption", "rand 0.8.5", "thiserror 1.0.69", + "tracing", ] [[package]] diff --git a/packages/rs-platform-wallet/Cargo.toml b/packages/rs-platform-wallet/Cargo.toml index c30e7e43e9a..0725b6a84d1 100644 --- a/packages/rs-platform-wallet/Cargo.toml +++ b/packages/rs-platform-wallet/Cargo.toml @@ -22,6 +22,7 @@ dashcore = { workspace = true } # Standard dependencies thiserror = "1.0" async-trait = "0.1" +tracing = "0.1" # Collections indexmap = "2.0" diff --git a/packages/rs-platform-wallet/src/platform_wallet_info/identity_discovery.rs b/packages/rs-platform-wallet/src/platform_wallet_info/identity_discovery.rs new file mode 100644 index 00000000000..3b50af51620 --- /dev/null +++ b/packages/rs-platform-wallet/src/platform_wallet_info/identity_discovery.rs @@ -0,0 +1,154 @@ +//! Gap-limit identity discovery for wallet sync +//! +//! This module implements DashSync-style gap-limit scanning for identities +//! during wallet sync. It derives consecutive authentication keys from the +//! wallet's BIP32 tree and queries Platform to find registered identities. + +use super::key_derivation::derive_identity_auth_key_hash; +use super::PlatformWalletInfo; +use crate::error::PlatformWalletError; +use dpp::identity::accessors::IdentityGettersV0; +use dpp::prelude::Identifier; +use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; + +impl PlatformWalletInfo { + /// Discover identities by scanning consecutive identity indices with a gap limit + /// + /// Starting from `start_index`, derives ECDSA authentication keys for consecutive + /// identity indices and queries Platform for registered identities. Scanning stops + /// when `gap_limit` consecutive indices yield no identity. + /// + /// This mirrors the DashSync gap-limit approach: keep scanning until N consecutive + /// misses, then stop. + /// + /// # Arguments + /// + /// * `wallet` - The wallet to derive authentication keys from + /// * `start_index` - The first identity index to check + /// * `gap_limit` - Number of consecutive misses before stopping (typically 5) + /// + /// # Returns + /// + /// Returns the list of newly discovered identity IDs + pub async fn discover_identities( + &mut self, + wallet: &key_wallet::Wallet, + start_index: u32, + gap_limit: u32, + ) -> Result, PlatformWalletError> { + use dash_sdk::platform::types::identity::PublicKeyHash; + use dash_sdk::platform::Fetch; + + let sdk = self + .identity_manager() + .sdk + .as_ref() + .ok_or_else(|| { + PlatformWalletError::InvalidIdentityData( + "SDK not configured in identity manager".to_string(), + ) + })? + .clone(); + + let network = self.network(); + let mut discovered = Vec::new(); + let mut consecutive_misses = 0u32; + let mut identity_index = start_index; + + while consecutive_misses < gap_limit { + // Derive the authentication key hash for this identity index (key_index 0) + let key_hash_array = derive_identity_auth_key_hash(wallet, network, identity_index, 0)?; + + // Query Platform for an identity registered with this key hash + match dpp::identity::Identity::fetch(&sdk, PublicKeyHash(key_hash_array)).await { + Ok(Some(identity)) => { + let identity_id = identity.id(); + + // Add to manager if not already present + if self.identity_manager().identity(&identity_id).is_none() { + self.identity_manager_mut().add_identity(identity)?; + discovered.push(identity_id); + } + consecutive_misses = 0; + } + Ok(None) => { + consecutive_misses += 1; + } + Err(e) => { + tracing::warn!( + identity_index, + "Failed to query identity by public key hash: {}", + e + ); + // Don't increment consecutive_misses: a query failure is not + // a confirmed empty slot and should not consume the gap budget. + } + } + + identity_index += 1; + } + + Ok(discovered) + } + + /// Discover identities and fetch their DashPay contact requests + /// + /// Calls [`discover_identities`] then fetches sent and received contact requests + /// for each discovered identity, storing them in the identity manager. + /// + /// # Arguments + /// + /// * `wallet` - The wallet to derive authentication keys from + /// * `start_index` - The first identity index to check + /// * `gap_limit` - Number of consecutive misses before stopping (typically 5) + /// + /// # Returns + /// + /// Returns the list of newly discovered identity IDs + pub async fn discover_identities_with_contacts( + &mut self, + wallet: &key_wallet::Wallet, + start_index: u32, + gap_limit: u32, + ) -> Result, PlatformWalletError> { + let discovered = self + .discover_identities(wallet, start_index, gap_limit) + .await?; + + if discovered.is_empty() { + return Ok(discovered); + } + + let sdk = self + .identity_manager() + .sdk + .as_ref() + .ok_or_else(|| { + PlatformWalletError::InvalidIdentityData( + "SDK not configured in identity manager".to_string(), + ) + })? + .clone(); + + for identity_id in &discovered { + // Get the identity from the manager to pass to the SDK + let identity = match self.identity_manager().identity(identity_id) { + Some(id) => id.clone(), + None => continue, + }; + + if let Err(e) = self + .fetch_and_store_contact_requests(&sdk, &identity, identity_id) + .await + { + tracing::warn!( + %identity_id, + "Failed to fetch contact requests during discovery: {}", + e + ); + } + } + + Ok(discovered) + } +} diff --git a/packages/rs-platform-wallet/src/platform_wallet_info/key_derivation.rs b/packages/rs-platform-wallet/src/platform_wallet_info/key_derivation.rs new file mode 100644 index 00000000000..87b1e63a5b2 --- /dev/null +++ b/packages/rs-platform-wallet/src/platform_wallet_info/key_derivation.rs @@ -0,0 +1,76 @@ +//! Shared key derivation utilities for identity authentication keys +//! +//! This module provides helper functions used by both the matured transactions +//! processor and the identity discovery scanner. + +use crate::error::PlatformWalletError; +use key_wallet::Network; + +/// Derive the 20-byte RIPEMD160(SHA256) hash of the public key at the given +/// identity authentication path. +/// +/// Path format: `base_path / identity_index' / key_index'` +/// where `base_path` is `m/9'/COIN_TYPE'/5'/0'` (mainnet or testnet). +/// +/// # Arguments +/// +/// * `wallet` - The wallet to derive keys from +/// * `network` - Network to select the correct coin type +/// * `identity_index` - The identity index (hardened) +/// * `key_index` - The key index within that identity (hardened) +/// +/// # Returns +/// +/// Returns the 20-byte public key hash suitable for Platform identity lookup. +pub(crate) fn derive_identity_auth_key_hash( + wallet: &key_wallet::Wallet, + network: Network, + identity_index: u32, + key_index: u32, +) -> Result<[u8; 20], PlatformWalletError> { + use dashcore::secp256k1::Secp256k1; + use dpp::util::hash::ripemd160_sha256; + use key_wallet::bip32::{ChildNumber, DerivationPath, ExtendedPubKey}; + use key_wallet::dip9::{ + IDENTITY_AUTHENTICATION_PATH_MAINNET, IDENTITY_AUTHENTICATION_PATH_TESTNET, + }; + + let base_path = match network { + Network::Dash => IDENTITY_AUTHENTICATION_PATH_MAINNET, + Network::Testnet => IDENTITY_AUTHENTICATION_PATH_TESTNET, + _ => { + return Err(PlatformWalletError::InvalidIdentityData( + "Unsupported network for identity derivation".to_string(), + )); + } + }; + + let mut full_path = DerivationPath::from(base_path); + full_path = full_path.extend([ + ChildNumber::from_hardened_idx(identity_index).map_err(|e| { + PlatformWalletError::InvalidIdentityData(format!("Invalid identity index: {}", e)) + })?, + ChildNumber::from_hardened_idx(key_index).map_err(|e| { + PlatformWalletError::InvalidIdentityData(format!("Invalid key index: {}", e)) + })?, + ]); + + let auth_key = wallet + .derive_extended_private_key(&full_path) + .map_err(|e| { + PlatformWalletError::InvalidIdentityData(format!( + "Failed to derive authentication key: {}", + e + )) + })?; + + let secp = Secp256k1::new(); + let public_key = ExtendedPubKey::from_priv(&secp, &auth_key); + let public_key_bytes = public_key.public_key.serialize(); + let key_hash = ripemd160_sha256(&public_key_bytes); + + let mut key_hash_array = [0u8; 20]; + key_hash_array.copy_from_slice(&key_hash); + + Ok(key_hash_array) +} diff --git a/packages/rs-platform-wallet/src/platform_wallet_info/matured_transactions.rs b/packages/rs-platform-wallet/src/platform_wallet_info/matured_transactions.rs index b5a3ceec052..c80f755b628 100644 --- a/packages/rs-platform-wallet/src/platform_wallet_info/matured_transactions.rs +++ b/packages/rs-platform-wallet/src/platform_wallet_info/matured_transactions.rs @@ -3,15 +3,12 @@ //! This module handles the detection and fetching of identities created from //! asset lock transactions. +use super::key_derivation::derive_identity_auth_key_hash; use super::PlatformWalletInfo; use crate::error::PlatformWalletError; -#[allow(unused_imports)] -use crate::ContactRequest; +use dpp::identity::accessors::IdentityGettersV0; use dpp::prelude::Identifier; use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; -use key_wallet::Network; - -use dpp::identity::accessors::IdentityGettersV0; impl PlatformWalletInfo { /// Discover identity and fetch contact requests for a single asset lock transaction @@ -62,7 +59,6 @@ impl PlatformWalletInfo { ) -> Result, PlatformWalletError> { use dash_sdk::platform::types::identity::PublicKeyHash; use dash_sdk::platform::Fetch; - use dpp::util::hash::ripemd160_sha256; let mut identities_processed = Vec::new(); @@ -83,59 +79,8 @@ impl PlatformWalletInfo { })? .clone(); - // Derive the first authentication key (identity_index 0, key_index 0) - let identity_index = 0u32; - let key_index = 0u32; - - // Build identity authentication derivation path - // Path format: m/9'/COIN_TYPE'/5'/0'/identity_index'/key_index' - use key_wallet::bip32::{ChildNumber, DerivationPath}; - use key_wallet::dip9::{ - IDENTITY_AUTHENTICATION_PATH_MAINNET, IDENTITY_AUTHENTICATION_PATH_TESTNET, - }; - - let base_path = match self.network() { - Network::Dash => IDENTITY_AUTHENTICATION_PATH_MAINNET, - Network::Testnet => IDENTITY_AUTHENTICATION_PATH_TESTNET, - _ => { - return Err(PlatformWalletError::InvalidIdentityData( - "Unsupported network for identity derivation".to_string(), - )); - } - }; - - // Create full derivation path: base path + identity_index' + key_index' - let mut full_path = DerivationPath::from(base_path); - full_path = full_path.extend([ - ChildNumber::from_hardened_idx(identity_index).map_err(|e| { - PlatformWalletError::InvalidIdentityData(format!("Invalid identity index: {}", e)) - })?, - ChildNumber::from_hardened_idx(key_index).map_err(|e| { - PlatformWalletError::InvalidIdentityData(format!("Invalid key index: {}", e)) - })?, - ]); - - // Derive the extended private key at this path - let auth_key = wallet - .derive_extended_private_key(&full_path) - .map_err(|e| { - PlatformWalletError::InvalidIdentityData(format!( - "Failed to derive authentication key: {}", - e - )) - })?; - - // Get public key bytes and hash them - use dashcore::secp256k1::Secp256k1; - use key_wallet::bip32::ExtendedPubKey; - let secp = Secp256k1::new(); - let public_key = ExtendedPubKey::from_priv(&secp, &auth_key); - let public_key_bytes = public_key.public_key.serialize(); - let key_hash = ripemd160_sha256(&public_key_bytes); - - // Create a fixed-size array from the hash - let mut key_hash_array = [0u8; 20]; - key_hash_array.copy_from_slice(&key_hash); + // Derive the first authentication key hash (identity_index 0, key_index 0) + let key_hash_array = derive_identity_auth_key_hash(wallet, self.network(), 0, 0)?; // Query Platform for identity by public key hash match dpp::identity::Identity::fetch(&sdk, PublicKeyHash(key_hash_array)).await { @@ -152,148 +97,27 @@ impl PlatformWalletInfo { } // Fetch DashPay contact requests for this identity - match sdk - .fetch_all_contact_requests_for_identity(&identity, Some(100)) + if let Err(e) = self + .fetch_and_store_contact_requests(&sdk, &identity, &identity_id) .await { - Ok((sent_docs, received_docs)) => { - // Process sent contact requests - for (_doc_id, maybe_doc) in sent_docs { - if let Some(doc) = maybe_doc { - if let Ok(contact_request) = parse_contact_request_document(&doc) { - // Add to managed identity - if let Some(managed_identity) = self - .identity_manager_mut() - .managed_identity_mut(&identity_id) - { - managed_identity.add_sent_contact_request(contact_request); - } - } - } - } - - // Process received contact requests - for (_doc_id, maybe_doc) in received_docs { - if let Some(doc) = maybe_doc { - if let Ok(contact_request) = parse_contact_request_document(&doc) { - // Add to managed identity - if let Some(managed_identity) = self - .identity_manager_mut() - .managed_identity_mut(&identity_id) - { - managed_identity - .add_incoming_contact_request(contact_request); - } - } - } - } - - identities_processed.push(identity_id); - } - Err(e) => { - eprintln!( - "Failed to fetch contact requests for identity {}: {}", - identity_id, e - ); - } + tracing::warn!( + %identity_id, + "Failed to fetch contact requests after asset lock: {}", + e + ); + } else { + identities_processed.push(identity_id); } } Ok(None) => { // No identity found for this key - that's ok, may not be registered yet } Err(e) => { - eprintln!("Failed to query identity by public key hash: {}", e); + tracing::warn!("Failed to query identity by public key hash: {}", e); } } Ok(identities_processed) } } - -/// Parse a contact request document into a ContactRequest struct -fn parse_contact_request_document( - doc: &dpp::document::Document, -) -> Result { - use dpp::document::DocumentV0Getters; - use dpp::platform_value::Value; - - // Extract fields from the document - let properties = doc.properties(); - - let to_user_id = properties - .get("toUserId") - .and_then(|v| match v { - Value::Identifier(id) => Some(Identifier::from(*id)), - _ => None, - }) - .ok_or_else(|| { - PlatformWalletError::InvalidIdentityData( - "Missing or invalid toUserId in contact request".to_string(), - ) - })?; - - let sender_key_index = properties - .get("senderKeyIndex") - .and_then(|v| match v { - Value::U32(i) => Some(*i), - _ => None, - }) - .ok_or_else(|| { - PlatformWalletError::InvalidIdentityData( - "Missing or invalid senderKeyIndex in contact request".to_string(), - ) - })?; - - let recipient_key_index = properties - .get("recipientKeyIndex") - .and_then(|v| match v { - Value::U32(i) => Some(*i), - _ => None, - }) - .ok_or_else(|| { - PlatformWalletError::InvalidIdentityData( - "Missing or invalid recipientKeyIndex in contact request".to_string(), - ) - })?; - - let account_reference = properties - .get("accountReference") - .and_then(|v| match v { - Value::U32(i) => Some(*i), - _ => None, - }) - .ok_or_else(|| { - PlatformWalletError::InvalidIdentityData( - "Missing or invalid accountReference in contact request".to_string(), - ) - })?; - - let encrypted_public_key = properties - .get("encryptedPublicKey") - .and_then(|v| match v { - Value::Bytes(b) => Some(b.clone()), - _ => None, - }) - .ok_or_else(|| { - PlatformWalletError::InvalidIdentityData( - "Missing or invalid encryptedPublicKey in contact request".to_string(), - ) - })?; - - let created_at_core_block_height = doc.created_at_core_block_height().unwrap_or(0); - - let created_at = doc.created_at().unwrap_or(0); - - let sender_id = doc.owner_id(); - - Ok(ContactRequest::new( - sender_id, - to_user_id, - sender_key_index, - recipient_key_index, - account_reference, - encrypted_public_key, - created_at_core_block_height, - created_at, - )) -} diff --git a/packages/rs-platform-wallet/src/platform_wallet_info/mod.rs b/packages/rs-platform-wallet/src/platform_wallet_info/mod.rs index 4c273f341f6..1efff631092 100644 --- a/packages/rs-platform-wallet/src/platform_wallet_info/mod.rs +++ b/packages/rs-platform-wallet/src/platform_wallet_info/mod.rs @@ -1,15 +1,23 @@ +use crate::error::PlatformWalletError; +use crate::ContactRequest; use crate::IdentityManager; +use dpp::prelude::Identifier; use key_wallet::wallet::ManagedWalletInfo; use key_wallet::Network; use std::fmt; mod accessors; mod contact_requests; +mod identity_discovery; +pub(crate) mod key_derivation; mod managed_account_operations; mod matured_transactions; mod wallet_info_interface; mod wallet_transaction_checker; +/// Default maximum number of contact request documents to fetch per identity. +const DEFAULT_CONTACT_REQUEST_LIMIT: u32 = 100; + /// Platform wallet information that extends ManagedWalletInfo with identity support #[derive(Clone)] pub struct PlatformWalletInfo { @@ -49,6 +57,174 @@ impl fmt::Debug for PlatformWalletInfo { } } +impl PlatformWalletInfo { + /// Fetch and store DashPay contact requests for a single identity. + /// + /// Queries Platform for sent and received contact request documents, + /// parses them, and stores them on the corresponding managed identity. + pub(super) async fn fetch_and_store_contact_requests( + &mut self, + sdk: &dash_sdk::Sdk, + identity: &dpp::identity::Identity, + identity_id: &Identifier, + ) -> Result<(), PlatformWalletError> { + let (sent_docs, received_docs) = sdk + .fetch_all_contact_requests_for_identity(identity, Some(DEFAULT_CONTACT_REQUEST_LIMIT)) + .await + .map_err(|e| { + PlatformWalletError::InvalidIdentityData(format!( + "Failed to fetch contact requests for identity {}: {}", + identity_id, e + )) + })?; + + // Process sent contact requests + for (_doc_id, maybe_doc) in sent_docs { + if let Some(doc) = maybe_doc { + match parse_contact_request_document(&doc) { + Ok(contact_request) => { + if let Some(managed_identity) = self + .identity_manager_mut() + .managed_identity_mut(identity_id) + { + managed_identity.add_sent_contact_request(contact_request); + } + } + Err(e) => { + tracing::warn!( + identity_id = %identity_id, + "Failed to parse sent contact request document: {}", + e + ); + } + } + } + } + + // Process received contact requests + for (_doc_id, maybe_doc) in received_docs { + if let Some(doc) = maybe_doc { + match parse_contact_request_document(&doc) { + Ok(contact_request) => { + if let Some(managed_identity) = self + .identity_manager_mut() + .managed_identity_mut(identity_id) + { + managed_identity.add_incoming_contact_request(contact_request); + } + } + Err(e) => { + tracing::warn!( + identity_id = %identity_id, + "Failed to parse received contact request document: {}", + e + ); + } + } + } + } + + Ok(()) + } +} + +/// Parse a contact request document into a ContactRequest struct +/// +/// Extracts DashPay contact request fields from a platform document. +pub(super) fn parse_contact_request_document( + doc: &dpp::document::Document, +) -> Result { + use dpp::document::DocumentV0Getters; + use dpp::platform_value::Value; + + let properties = doc.properties(); + + let to_user_id = properties + .get("toUserId") + .and_then(|v| match v { + Value::Identifier(id) => Some(Identifier::from(*id)), + _ => None, + }) + .ok_or_else(|| { + PlatformWalletError::InvalidIdentityData( + "Missing or invalid toUserId in contact request".to_string(), + ) + })?; + + let sender_key_index = properties + .get("senderKeyIndex") + .and_then(|v| match v { + Value::U32(i) => Some(*i), + _ => None, + }) + .ok_or_else(|| { + PlatformWalletError::InvalidIdentityData( + "Missing or invalid senderKeyIndex in contact request".to_string(), + ) + })?; + + let recipient_key_index = properties + .get("recipientKeyIndex") + .and_then(|v| match v { + Value::U32(i) => Some(*i), + _ => None, + }) + .ok_or_else(|| { + PlatformWalletError::InvalidIdentityData( + "Missing or invalid recipientKeyIndex in contact request".to_string(), + ) + })?; + + let account_reference = properties + .get("accountReference") + .and_then(|v| match v { + Value::U32(i) => Some(*i), + _ => None, + }) + .ok_or_else(|| { + PlatformWalletError::InvalidIdentityData( + "Missing or invalid accountReference in contact request".to_string(), + ) + })?; + + let encrypted_public_key = properties + .get("encryptedPublicKey") + .and_then(|v| match v { + Value::Bytes(b) => Some(b.clone()), + _ => None, + }) + .ok_or_else(|| { + PlatformWalletError::InvalidIdentityData( + "Missing or invalid encryptedPublicKey in contact request".to_string(), + ) + })?; + + let created_at_core_block_height = doc.created_at_core_block_height().ok_or_else(|| { + PlatformWalletError::InvalidIdentityData( + "Missing created_at_core_block_height in contact request".to_string(), + ) + })?; + + let created_at = doc.created_at().ok_or_else(|| { + PlatformWalletError::InvalidIdentityData( + "Missing created_at in contact request".to_string(), + ) + })?; + + let sender_id = doc.owner_id(); + + Ok(ContactRequest::new( + sender_id, + to_user_id, + sender_key_index, + recipient_key_index, + account_reference, + encrypted_public_key, + created_at_core_block_height, + created_at, + )) +} + #[cfg(test)] mod tests { use crate::platform_wallet_info::PlatformWalletInfo; diff --git a/packages/rs-platform-wallet/src/platform_wallet_info/wallet_transaction_checker.rs b/packages/rs-platform-wallet/src/platform_wallet_info/wallet_transaction_checker.rs index 7c5de27928d..1d27e593e17 100644 --- a/packages/rs-platform-wallet/src/platform_wallet_info/wallet_transaction_checker.rs +++ b/packages/rs-platform-wallet/src/platform_wallet_info/wallet_transaction_checker.rs @@ -37,7 +37,7 @@ impl WalletTransactionChecker for PlatformWalletInfo { .fetch_identity_and_contacts_for_asset_lock(wallet, tx) .await { - eprintln!("Failed to fetch identity for asset lock: {}", e); + tracing::warn!("Failed to fetch identity for asset lock: {}", e); } } }