From 163c924d44273344be8fe1bc5f77a5acd2ce7612 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 5 Mar 2026 14:20:58 +0700 Subject: [PATCH] feat(platform-wallet): add gap-limit identity discovery scan Add DashSync-style identity discovery to PlatformWalletInfo that scans consecutive DIP-13 authentication key indices and queries Platform to find registered identities during wallet sync. New methods: - discover_identities: gap-limit scan using PublicKeyHash queries - discover_identities_with_contacts: same + fetches DashPay contacts Refactors shared key derivation and contact request parsing into reusable modules used by both discovery and asset lock processing. Co-Authored-By: Claude Opus 4.6 --- .../identity_discovery.rs | 189 ++++++++++++++++++ .../platform_wallet_info/key_derivation.rs | 76 +++++++ .../matured_transactions.rs | 153 +------------- .../src/platform_wallet_info/mod.rs | 94 +++++++++ 4 files changed, 365 insertions(+), 147 deletions(-) create mode 100644 packages/rs-platform-wallet/src/platform_wallet_info/identity_discovery.rs create mode 100644 packages/rs-platform-wallet/src/platform_wallet_info/key_derivation.rs 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..fafd2d89b82 --- /dev/null +++ b/packages/rs-platform-wallet/src/platform_wallet_info/identity_discovery.rs @@ -0,0 +1,189 @@ +//! 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::parse_contact_request_document; +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() + .identities() + .contains_key(&identity_id) + { + self.identity_manager_mut().add_identity(identity)?; + } + + discovered.push(identity_id); + consecutive_misses = 0; + } + Ok(None) => { + consecutive_misses += 1; + } + Err(e) => { + eprintln!( + "Failed to query identity by public key hash at index {}: {}", + identity_index, e + ); + consecutive_misses += 1; + } + } + + 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, + }; + + match sdk + .fetch_all_contact_requests_for_identity(&identity, Some(100)) + .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) { + 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) { + if let Some(managed_identity) = self + .identity_manager_mut() + .managed_identity_mut(identity_id) + { + managed_identity.add_incoming_contact_request(contact_request); + } + } + } + } + } + Err(e) => { + eprintln!( + "Failed to fetch contact requests for identity {}: {}", + identity_id, 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..4c23665b3e1 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,13 @@ //! 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::parse_contact_request_document; 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 +60,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 +80,9 @@ 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 { @@ -209,91 +156,3 @@ impl PlatformWalletInfo { 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..78b2076c4ae 100644 --- a/packages/rs-platform-wallet/src/platform_wallet_info/mod.rs +++ b/packages/rs-platform-wallet/src/platform_wallet_info/mod.rs @@ -1,10 +1,15 @@ +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; @@ -49,6 +54,95 @@ impl fmt::Debug for PlatformWalletInfo { } } +/// 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().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, + )) +} + #[cfg(test)] mod tests { use crate::platform_wallet_info::PlatformWalletInfo;