From 18e25cdc1ceffc9d25072a12ce8b439efb181db7 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Thu, 8 Jan 2026 15:44:06 +0100 Subject: [PATCH 01/10] feat(dpp): Identity::new_with_input_addresses_and_keys() --- packages/rs-dpp/src/identity/identity.rs | 27 +++++++++- ...tate_transition_identity_id_from_inputs.rs | 54 +++++++++++-------- .../v0/mod.rs | 4 +- 3 files changed, 59 insertions(+), 26 deletions(-) diff --git a/packages/rs-dpp/src/identity/identity.rs b/packages/rs-dpp/src/identity/identity.rs index 8c2f57ad678..b521295a461 100644 --- a/packages/rs-dpp/src/identity/identity.rs +++ b/packages/rs-dpp/src/identity/identity.rs @@ -1,6 +1,8 @@ +use crate::address_funds::PlatformAddress; use crate::identity::v0::IdentityV0; use crate::identity::{IdentityPublicKey, KeyID}; -use crate::prelude::Revision; +use crate::prelude::{AddressNonce, Revision}; +use crate::state_transition::identity_id_from_input_addresses; #[cfg(feature = "identity-hashing")] use crate::serialization::PlatformSerializable; @@ -118,6 +120,29 @@ impl Identity { } } + /// Create a new identity using input [PlatformAddress]es. + /// + /// This function derives the identity ID from the provided input addresses. + /// + /// ## Arguments + /// + /// * `inputs` - A map of PlatformAddress to AddressNonce tuples used to derive the identity id; the nonces + /// should represent state after creation of the identity (eg. be incremented by 1). + /// * `public_keys` - A map of KeyID to IdentityPublicKey tuples representing the public keys for the identity. + /// * `platform_version` - The platform version to use for identity creation. + /// + /// ## Returns + /// + /// * `Result` - Returns the newly created Identity or a ProtocolError if the operation fails. + pub fn new_with_input_addresses_and_keys( + inputs: &BTreeMap, + public_keys: BTreeMap, + platform_version: &PlatformVersion, + ) -> Result { + let identity_id = identity_id_from_input_addresses(inputs)?; + Self::new_with_id_and_keys(identity_id, public_keys, platform_version) + } + /// Convenience method to get Partial Identity Info pub fn into_partial_identity_info(self) -> PartialIdentity { match self { diff --git a/packages/rs-dpp/src/state_transition/traits/state_transition_identity_id_from_inputs.rs b/packages/rs-dpp/src/state_transition/traits/state_transition_identity_id_from_inputs.rs index 9f64b21ef3d..628740687f2 100644 --- a/packages/rs-dpp/src/state_transition/traits/state_transition_identity_id_from_inputs.rs +++ b/packages/rs-dpp/src/state_transition/traits/state_transition_identity_id_from_inputs.rs @@ -1,36 +1,44 @@ use std::collections::BTreeMap; use crate::address_funds::PlatformAddress; +use crate::fee::Credits; use crate::prelude::AddressNonce; use crate::state_transition::StateTransitionWitnessSigned; +use crate::util::hash::hash_double; use crate::ProtocolError; use platform_value::Identifier; pub trait StateTransitionIdentityIdFromInputs: StateTransitionWitnessSigned { - /// Get the identity id from inputs + /// Get the identity id from inputs. + /// + /// Inputs should represent state after creation of the identity (eg. be incremented by 1). fn identity_id_from_inputs(&self) -> Result { - if self.inputs().is_empty() { - return Err(ProtocolError::ParsingError( - "Identity creation requires at least one input".to_string(), - )); - } - - // Build a map containing only (PlatformAddress, KeyOfTypeNonce) pairs, - // ignoring the Credits in the input values. - let address_nonce_map: BTreeMap<&PlatformAddress, &AddressNonce> = self - .inputs() - .iter() - .map(|(address, (nonce, _credits))| (address, nonce)) - .collect(); - - use crate::util::hash::hash_double; - - let input_bytes = bincode::encode_to_vec(&address_nonce_map, bincode::config::standard()) - .map_err(|e| { - ProtocolError::EncodingError(format!("Failed to encode inputs: {}", e)) - })?; + let inputs = self.inputs(); + identity_id_from_input_addresses(inputs) + } +} - let hash = hash_double(input_bytes); - Ok(Identifier::new(hash)) +/// Helper that computes the identity ID from input addresses and nonces. +/// Nonces should represent state after creation of the identity (eg. be incremented by 1). +/// +/// Internal use only; see `StateTransitionIdentityIdFromInputs` trait. +pub(crate) fn identity_id_from_input_addresses( + input_addresses: &BTreeMap, +) -> Result { + if input_addresses.is_empty() { + return Err(ProtocolError::ParsingError( + "Identity creation requires at least one input".to_string(), + )); } + // Build a map containing only (PlatformAddress, KeyOfTypeNonce) pairs, + // ignoring the Credits in the input values. + let address_nonce_map: BTreeMap<&PlatformAddress, &AddressNonce> = input_addresses + .iter() + .map(|(address, (nonce, _credits))| (address, nonce)) + .collect(); + let input_bytes = bincode::encode_to_vec(&address_nonce_map, bincode::config::standard()) + .map_err(|e| ProtocolError::EncodingError(format!("Failed to encode inputs: {}", e)))?; + + let hash = hash_double(input_bytes); + Ok(Identifier::new(hash)) } diff --git a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs index 9f5c8b662ce..c29f56d812a 100644 --- a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs +++ b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs @@ -989,7 +989,7 @@ impl Drive { })?; let (root_hash_identity, identity) = Drive::verify_full_identity_by_identity_id( proof, - false, + true, identity_id.into_buffer(), platform_version, )?; @@ -1006,7 +1006,7 @@ impl Drive { ) = Drive::verify_addresses_infos( proof, addresses_to_check, - false, + true, platform_version, )?; From d31216929b62590a48836401fff58377bb6dfd9f Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Thu, 8 Jan 2026 16:08:42 +0100 Subject: [PATCH 02/10] chore(sdk): remove Identity::put_with_address_funding() --- .../src/platform/transition/put_identity.rs | 40 +++---------------- 1 file changed, 6 insertions(+), 34 deletions(-) diff --git a/packages/rs-sdk/src/platform/transition/put_identity.rs b/packages/rs-sdk/src/platform/transition/put_identity.rs index fc04d242b41..c90a683aec2 100644 --- a/packages/rs-sdk/src/platform/transition/put_identity.rs +++ b/packages/rs-sdk/src/platform/transition/put_identity.rs @@ -1,7 +1,6 @@ use crate::platform::transition::broadcast_identity::BroadcastRequestForNewIdentity; use crate::platform::transition::{ - address_inputs::{collect_address_infos_from_proof, fetch_inputs_with_nonce, nonce_inc}, - broadcast::BroadcastStateTransition, + address_inputs::collect_address_infos_from_proof, broadcast::BroadcastStateTransition, }; use crate::{Error, Sdk}; @@ -47,18 +46,12 @@ pub trait PutIdentity>: Waitable { where Self: Sized; - /// Creates an identity funded by Platform addresses (nonces fetched automatically). - async fn put_with_address_funding + Send + Sync>( - &self, - sdk: &Sdk, - inputs: BTreeMap, - output: Option<(PlatformAddress, Credits)>, - identity_signer: &IS, - input_address_signer: &AS, - settings: Option, - ) -> Result<(Identity, AddressInfos), Error>; - /// Creates an identity funded by Platform addresses using explicit nonces. + /// + /// Use [Identity::new_with_input_addresses_and_keys](dpp::identity::Identity::new_with_input_addresses_and_keys) + /// to create an identity. Then use this method to put it to the platform. + /// + /// This is a preferred method, as you need to use the same nonces when creating the identity. async fn put_with_address_funding_with_nonce + Send + Sync>( &self, sdk: &Sdk, @@ -112,27 +105,6 @@ impl> PutIdentity for Identity { Self::wait_for_response(sdk, state_transition, settings).await } - async fn put_with_address_funding + Send + Sync>( - &self, - sdk: &Sdk, - inputs: BTreeMap, - output: Option<(PlatformAddress, Credits)>, - identity_signer: &IS, - input_address_signer: &AS, - settings: Option, - ) -> Result<(Identity, AddressInfos), Error> { - let inputs_with_nonce = nonce_inc(fetch_inputs_with_nonce(sdk, &inputs).await?); - self.put_with_address_funding_with_nonce( - sdk, - inputs_with_nonce, - output, - identity_signer, - input_address_signer, - settings, - ) - .await - } - async fn put_with_address_funding_with_nonce + Send + Sync>( &self, sdk: &Sdk, From afe0e7de5cbe05ef7be31db6e391a7f5ee0f2c0a Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Fri, 9 Jan 2026 11:02:04 +0100 Subject: [PATCH 03/10] fix wasm-sdk build --- .../src/platform/transition/put_identity.rs | 4 +- .../src/state_transitions/addresses.rs | 62 ++++++++++++++++++- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/packages/rs-sdk/src/platform/transition/put_identity.rs b/packages/rs-sdk/src/platform/transition/put_identity.rs index c90a683aec2..42b233efe5b 100644 --- a/packages/rs-sdk/src/platform/transition/put_identity.rs +++ b/packages/rs-sdk/src/platform/transition/put_identity.rs @@ -52,7 +52,7 @@ pub trait PutIdentity>: Waitable { /// to create an identity. Then use this method to put it to the platform. /// /// This is a preferred method, as you need to use the same nonces when creating the identity. - async fn put_with_address_funding_with_nonce + Send + Sync>( + async fn put_with_address_funding + Send + Sync>( &self, sdk: &Sdk, inputs_with_nonce: BTreeMap, @@ -105,7 +105,7 @@ impl> PutIdentity for Identity { Self::wait_for_response(sdk, state_transition, settings).await } - async fn put_with_address_funding_with_nonce + Send + Sync>( + async fn put_with_address_funding + Send + Sync>( &self, sdk: &Sdk, inputs: BTreeMap, diff --git a/packages/wasm-sdk/src/state_transitions/addresses.rs b/packages/wasm-sdk/src/state_transitions/addresses.rs index f681831b967..a2a564ad7f1 100644 --- a/packages/wasm-sdk/src/state_transitions/addresses.rs +++ b/packages/wasm-sdk/src/state_transitions/addresses.rs @@ -2,6 +2,8 @@ //! //! This module provides WASM bindings for address fund operations like transfers and withdrawals. +use std::collections::{BTreeMap, BTreeSet}; + use crate::error::WasmSdkError; use crate::queries::address::PlatformAddressInfoWasm; use crate::queries::utils::deserialize_required_query; @@ -13,7 +15,10 @@ use dash_sdk::platform::transition::address_credit_withdrawal::WithdrawAddressFu use dash_sdk::platform::transition::top_up_identity_from_addresses::TopUpIdentityFromAddresses; use dash_sdk::platform::transition::transfer_address_funds::TransferAddressFunds; use dash_sdk::platform::transition::transfer_to_addresses::TransferToAddresses; -use dash_sdk::platform::{Fetch, Identifier, Identity}; +use dash_sdk::platform::{Fetch, FetchMany, Identifier, Identity}; +use dash_sdk::Sdk; +use drive::dpp::fee::Credits; +use drive::dpp::prelude::AddressNonce; use drive_proof_verifier::types::{AddressInfo, IndexMap}; use js_sys::{BigInt, Map}; use serde::Deserialize; @@ -968,7 +973,8 @@ impl WasmSdk { // Convert inputs to map (address -> amount) let inputs_map = outputs_to_btree_map(parsed.inputs); - + // Extend inputs with nonces + let inputs = fetch_nonces_into_address_map(self.inner_sdk(), inputs_map).await?; // Convert change output if provided let change_output = parsed.change_output.map(|output| output.into_inner()); @@ -987,7 +993,7 @@ impl WasmSdk { let (created_identity, address_infos) = identity .put_with_address_funding( self.inner_sdk(), - inputs_map, + inputs, change_output, &identity_signer, &address_signer, @@ -1004,3 +1010,53 @@ impl WasmSdk { }) } } + +/// Fetch nonces for a set of Platform addresses and merge into inputs map. +/// +/// Credits provided in the inputs_map are preserved. +/// +/// Returns error when any address is not found or empty. +async fn fetch_nonces_into_address_map( + sdk: &Sdk, + inputs_map: BTreeMap, +) -> Result, WasmSdkError> { + // collect addresses + let input_addresses: BTreeSet = + inputs_map.keys().cloned().collect::>(); + + // fetch nonces + let fetched_addresses = dash_sdk::query_types::AddressInfo::fetch_many(sdk, input_addresses) + .await + .map_err(|e| { + WasmSdkError::generic(format!( + "Failed to fetch address infos for identity creation: {}", + e + )) + })? + .into_iter() + .filter_map(|(k, v)| v.map(|info| (k, info))) + .collect::>(); + + // sanity check + if inputs_map.len() != fetched_addresses.len() { + return Err(WasmSdkError::generic( + "Some input addresses were not found or are empty when fetching nonces", + )); + } + // merge nonces into inputs map + let inputs = inputs_map + .into_iter() + .zip(fetched_addresses) + .map(|((address_left, amount), (address_right, info))| { + if address_left != address_right { + Err(WasmSdkError::generic( + "Address mismatch when merging nonces for identity creation; platform bug?", + ))? + } + let nonce = info.nonce; + Ok((address_left, (nonce, amount))) + }) + .collect::, WasmSdkError>>()?; + + Ok(inputs) +} From f24f6c373c8d5782a57ec49d17c70994540573ee Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Fri, 9 Jan 2026 12:03:02 +0100 Subject: [PATCH 04/10] chore: fix build --- packages/rs-dpp/src/identity/identity.rs | 4 +++- packages/wasm-sdk/src/state_transitions/addresses.rs | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/rs-dpp/src/identity/identity.rs b/packages/rs-dpp/src/identity/identity.rs index b521295a461..ea5a4a63b67 100644 --- a/packages/rs-dpp/src/identity/identity.rs +++ b/packages/rs-dpp/src/identity/identity.rs @@ -2,7 +2,6 @@ use crate::address_funds::PlatformAddress; use crate::identity::v0::IdentityV0; use crate::identity::{IdentityPublicKey, KeyID}; use crate::prelude::{AddressNonce, Revision}; -use crate::state_transition::identity_id_from_input_addresses; #[cfg(feature = "identity-hashing")] use crate::serialization::PlatformSerializable; @@ -134,11 +133,14 @@ impl Identity { /// ## Returns /// /// * `Result` - Returns the newly created Identity or a ProtocolError if the operation fails. + #[cfg(feature = "state-transitions")] pub fn new_with_input_addresses_and_keys( inputs: &BTreeMap, public_keys: BTreeMap, platform_version: &PlatformVersion, ) -> Result { + use crate::state_transition::identity_id_from_input_addresses; + let identity_id = identity_id_from_input_addresses(inputs)?; Self::new_with_id_and_keys(identity_id, public_keys, platform_version) } diff --git a/packages/wasm-sdk/src/state_transitions/addresses.rs b/packages/wasm-sdk/src/state_transitions/addresses.rs index a2a564ad7f1..4dd6b941589 100644 --- a/packages/wasm-sdk/src/state_transitions/addresses.rs +++ b/packages/wasm-sdk/src/state_transitions/addresses.rs @@ -10,15 +10,15 @@ use crate::queries::utils::deserialize_required_query; use crate::sdk::WasmSdk; use crate::settings::{parse_put_settings, PutSettingsJs}; use dash_sdk::dpp::address_funds::PlatformAddress; +use dash_sdk::dpp::fee::Credits; use dash_sdk::dpp::identity::core_script::CoreScript; +use dash_sdk::dpp::prelude::AddressNonce; use dash_sdk::platform::transition::address_credit_withdrawal::WithdrawAddressFunds; use dash_sdk::platform::transition::top_up_identity_from_addresses::TopUpIdentityFromAddresses; use dash_sdk::platform::transition::transfer_address_funds::TransferAddressFunds; use dash_sdk::platform::transition::transfer_to_addresses::TransferToAddresses; use dash_sdk::platform::{Fetch, FetchMany, Identifier, Identity}; use dash_sdk::Sdk; -use drive::dpp::fee::Credits; -use drive::dpp::prelude::AddressNonce; use drive_proof_verifier::types::{AddressInfo, IndexMap}; use js_sys::{BigInt, Map}; use serde::Deserialize; From 5d227d89674895cb8611e79b60cb3f8c97f2797b Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Fri, 9 Jan 2026 13:49:40 +0100 Subject: [PATCH 05/10] chore: apply rabbit feedback --- packages/rs-dpp/src/identity/identity.rs | 5 ++- .../src/state_transitions/addresses.rs | 37 ++++++++++++------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/packages/rs-dpp/src/identity/identity.rs b/packages/rs-dpp/src/identity/identity.rs index ea5a4a63b67..d469f16203b 100644 --- a/packages/rs-dpp/src/identity/identity.rs +++ b/packages/rs-dpp/src/identity/identity.rs @@ -125,8 +125,9 @@ impl Identity { /// /// ## Arguments /// - /// * `inputs` - A map of PlatformAddress to AddressNonce tuples used to derive the identity id; the nonces - /// should represent state after creation of the identity (eg. be incremented by 1). + /// * `inputs` - A map of `PlatformAddress` to `(AddressNonce, Credits)`. + /// The identity id is derived from the addresses and nonces (credits are ignored for the id derivation). + /// The nonces should represent state after creation of the identity (e.g. be incremented by 1). /// * `public_keys` - A map of KeyID to IdentityPublicKey tuples representing the public keys for the identity. /// * `platform_version` - The platform version to use for identity creation. /// diff --git a/packages/wasm-sdk/src/state_transitions/addresses.rs b/packages/wasm-sdk/src/state_transitions/addresses.rs index 4dd6b941589..7270ef84735 100644 --- a/packages/wasm-sdk/src/state_transitions/addresses.rs +++ b/packages/wasm-sdk/src/state_transitions/addresses.rs @@ -1011,11 +1011,11 @@ impl WasmSdk { } } -/// Fetch nonces for a set of Platform addresses and merge into inputs map. +/// Fetch nonces for a set of Platform addresses and merge into provided map. /// /// Credits provided in the inputs_map are preserved. /// -/// Returns error when any address is not found or empty. +/// Returns error when any address is not found or has insufficient balance. async fn fetch_nonces_into_address_map( sdk: &Sdk, inputs_map: BTreeMap, @@ -1037,25 +1037,34 @@ async fn fetch_nonces_into_address_map( .filter_map(|(k, v)| v.map(|info| (k, info))) .collect::>(); - // sanity check + // sanity check - filter_map above shouuld have removed any non-existing addresses if inputs_map.len() != fetched_addresses.len() { - return Err(WasmSdkError::generic( - "Some input addresses were not found or are empty when fetching nonces", + return Err(WasmSdkError::invalid_argument( + "Some input addresses were not found when fetching nonces", )); } // merge nonces into inputs map let inputs = inputs_map .into_iter() .zip(fetched_addresses) - .map(|((address_left, amount), (address_right, info))| { - if address_left != address_right { - Err(WasmSdkError::generic( - "Address mismatch when merging nonces for identity creation; platform bug?", - ))? - } - let nonce = info.nonce; - Ok((address_left, (nonce, amount))) - }) + .map( + |((address_requested, amount), (address_received, info_received))| { + if address_requested != address_received { + Err(WasmSdkError::invalid_argument( + format!("Address mismatch when merging nonces for identity creation ({} vs {}); platform bug?", + address_requested, address_received) + ))? + } + if amount > info_received.balance { + Err(WasmSdkError::invalid_argument(format!( + "Input address {} has insufficient balance: requested {}, available {}", + address_requested, amount, info_received.balance + )))? + } + let nonce = info_received.nonce; + Ok((address_requested, (nonce, amount))) + }, + ) .collect::, WasmSdkError>>()?; Ok(inputs) From b2d53f3edf17609e6dbe71d5e0b41c5b2951df39 Mon Sep 17 00:00:00 2001 From: lklimek <842586+lklimek@users.noreply.github.com> Date: Fri, 9 Jan 2026 15:31:06 +0100 Subject: [PATCH 06/10] typo Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- packages/wasm-sdk/src/state_transitions/addresses.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wasm-sdk/src/state_transitions/addresses.rs b/packages/wasm-sdk/src/state_transitions/addresses.rs index 7270ef84735..5afe6d02c25 100644 --- a/packages/wasm-sdk/src/state_transitions/addresses.rs +++ b/packages/wasm-sdk/src/state_transitions/addresses.rs @@ -1037,7 +1037,7 @@ async fn fetch_nonces_into_address_map( .filter_map(|(k, v)| v.map(|info| (k, info))) .collect::>(); - // sanity check - filter_map above shouuld have removed any non-existing addresses + // sanity check - filter_map above should have removed any non-existing addresses if inputs_map.len() != fetched_addresses.len() { return Err(WasmSdkError::invalid_argument( "Some input addresses were not found when fetching nonces", From f1301710ea06de5b5245cf1cabba43c6b45bb5d0 Mon Sep 17 00:00:00 2001 From: lklimek <842586+lklimek@users.noreply.github.com> Date: Tue, 13 Jan 2026 11:41:38 +0100 Subject: [PATCH 07/10] feat(sdk): sync platform address nonces (#2979) --- packages/rs-sdk-ffi/src/address_sync/mod.rs | 5 +- .../rs-sdk-ffi/src/address_sync/provider.rs | 17 ++++- packages/rs-sdk-ffi/src/address_sync/types.rs | 3 + .../rs-sdk/src/platform/address_sync/mod.rs | 73 ++++++++++++------- .../src/platform/address_sync/provider.rs | 10 +-- .../rs-sdk/src/platform/address_sync/types.rs | 22 ++++-- 6 files changed, 89 insertions(+), 41 deletions(-) diff --git a/packages/rs-sdk-ffi/src/address_sync/mod.rs b/packages/rs-sdk-ffi/src/address_sync/mod.rs index 95df2fa7d49..9f83d91d351 100644 --- a/packages/rs-sdk-ffi/src/address_sync/mod.rs +++ b/packages/rs-sdk-ffi/src/address_sync/mod.rs @@ -164,7 +164,7 @@ pub unsafe extern "C" fn dash_sdk_sync_address_balances_with_result( fn convert_sync_result(result: AddressSyncResult) -> DashSDKAddressSyncResult { // Convert found addresses let mut found_entries: Vec = Vec::with_capacity(result.found.len()); - for ((index, key), balance) in result.found.iter() { + for ((index, key), funds) in result.found.iter() { let key_data = key.clone().into_boxed_slice(); let key_len = key_data.len(); let key_ptr = Box::into_raw(key_data) as *mut u8; @@ -173,7 +173,8 @@ fn convert_sync_result(result: AddressSyncResult) -> DashSDKAddressSyncResult { index: *index, key: key_ptr, key_len, - balance: *balance, + nonce: funds.nonce, + balance: funds.balance, }); } diff --git a/packages/rs-sdk-ffi/src/address_sync/provider.rs b/packages/rs-sdk-ffi/src/address_sync/provider.rs index d92bb411ec6..44f5e3de7e4 100644 --- a/packages/rs-sdk-ffi/src/address_sync/provider.rs +++ b/packages/rs-sdk-ffi/src/address_sync/provider.rs @@ -1,7 +1,7 @@ //! FFI-compatible address provider implementation using callbacks use super::types::DashSDKPendingAddressList; -use dash_sdk::platform::address_sync::{AddressIndex, AddressKey, AddressProvider}; +use dash_sdk::platform::address_sync::{AddressFunds, AddressIndex, AddressKey, AddressProvider}; use std::os::raw::c_void; /// Function pointer type for getting pending addresses @@ -25,12 +25,13 @@ pub type GetHighestFoundIndexFn = unsafe extern "C" fn(context: *mut c_void) -> /// Function pointer type for handling a found address /// -/// Called when an address is found in the tree with a balance. +/// Called when an address is found in the tree with a balance and nonce. pub type OnAddressFoundFn = unsafe extern "C" fn( context: *mut c_void, index: u32, key: *const u8, key_len: usize, + nonce: u32, balance: u64, ); @@ -126,10 +127,17 @@ impl<'a> AddressProvider for CallbackAddressProvider<'a> { } } - fn on_address_found(&mut self, index: AddressIndex, key: &[u8], balance: u64) { + fn on_address_found(&mut self, index: AddressIndex, key: &[u8], funds: AddressFunds) { unsafe { let vtable = &*self.ffi.vtable; - (vtable.on_address_found)(self.ffi.context, index, key.as_ptr(), key.len(), balance); + (vtable.on_address_found)( + self.ffi.context, + index, + key.as_ptr(), + key.len(), + funds.nonce, + funds.balance, + ); } } @@ -214,6 +222,7 @@ mod tests { _index: u32, _key: *const u8, _key_len: usize, + _nonce: u32, _balance: u64, ) { } diff --git a/packages/rs-sdk-ffi/src/address_sync/types.rs b/packages/rs-sdk-ffi/src/address_sync/types.rs index 84e8973eaf2..07defe09177 100644 --- a/packages/rs-sdk-ffi/src/address_sync/types.rs +++ b/packages/rs-sdk-ffi/src/address_sync/types.rs @@ -76,6 +76,9 @@ pub struct DashSDKFoundAddress { /// Length of the key in bytes pub key_len: usize, + /// Nonce associated with this address + pub nonce: u32, + /// Balance in credits at this address pub balance: u64, } diff --git a/packages/rs-sdk/src/platform/address_sync/mod.rs b/packages/rs-sdk/src/platform/address_sync/mod.rs index 4df63c61915..0c5b41bd4c2 100644 --- a/packages/rs-sdk/src/platform/address_sync/mod.rs +++ b/packages/rs-sdk/src/platform/address_sync/mod.rs @@ -42,8 +42,8 @@ mod types; pub use provider::AddressProvider; pub use types::{ - AddressIndex, AddressKey, AddressSyncConfig, AddressSyncMetrics, AddressSyncResult, - LeafBoundaryKey, + AddressFunds, AddressIndex, AddressKey, AddressSyncConfig, AddressSyncMetrics, + AddressSyncResult, LeafBoundaryKey, }; use crate::error::Error; @@ -54,6 +54,7 @@ use dapi_grpc::platform::v0::{ get_addresses_branch_state_request, get_addresses_branch_state_response, GetAddressesBranchStateRequest, }; +use dpp::prelude::AddressNonce; use dpp::version::PlatformVersion; use drive::drive::Drive; use drive::grovedb::{ @@ -81,7 +82,7 @@ use tracker::KeyLeafTracker; /// - `config`: Optional configuration; uses defaults if `None`. /// /// # Returns -/// - `Ok(AddressSyncResult)`: Contains found addresses with balances and absent addresses. +/// - `Ok(AddressSyncResult)`: Contains found addresses with balances/nonces and absent addresses. /// - `Err(Error)`: If the sync fails after exhausting retries. /// /// # Example @@ -130,7 +131,7 @@ pub async fn sync_address_balances( // Step 2: Process trunk result let mut tracker = KeyLeafTracker::new(); - process_trunk_result(&trunk_result, provider, &mut result, &mut tracker); + process_trunk_result(&trunk_result, provider, &mut result, &mut tracker)?; // Step 3: Iterative branch queries let min_query_depth = platform_version @@ -190,7 +191,7 @@ pub async fn sync_address_balances( &key_to_index, &mut result, &mut tracker, - ); + )?; } // Check if provider has extended pending addresses (gap limit behavior) @@ -245,16 +246,16 @@ fn process_trunk_result( provider: &mut P, result: &mut AddressSyncResult, tracker: &mut KeyLeafTracker, -) { +) -> Result<(), Error> { // Get pending addresses let pending: Vec<(AddressIndex, AddressKey)> = provider.pending_addresses(); for (index, key) in pending { // Check if found in elements if let Some(element) = trunk_result.elements.get(&key) { - let balance = extract_balance_from_element(element); - result.found.insert((index, key.clone()), balance); - provider.on_address_found(index, &key, balance); + let funds = AddressFunds::try_from(element)?; + result.found.insert((index, key.clone()), funds); + provider.on_address_found(index, &key, funds); } else { // Trace to leaf if let Some((leaf_key, info)) = trunk_result.trace_key_to_leaf(&key) { @@ -266,6 +267,8 @@ fn process_trunk_result( } } } + + Ok(()) } /// Get privacy-adjusted leaves to query. @@ -471,7 +474,7 @@ fn process_branch_result( key_to_index: &HashMap, result: &mut AddressSyncResult, tracker: &mut KeyLeafTracker, -) { +) -> Result<(), Error> { // Get all target keys that were in this leaf's subtree let target_keys = tracker.keys_for_leaf(queried_leaf_key); @@ -480,9 +483,9 @@ fn process_branch_result( // Check if found in elements if let Some(element) = branch_result.elements.get(&target_key) { - let balance = extract_balance_from_element(element); - result.found.insert((index, target_key.clone()), balance); - provider.on_address_found(index, &target_key, balance); + let funds = AddressFunds::try_from(element)?; + result.found.insert((index, target_key.clone()), funds); + provider.on_address_found(index, &target_key, funds); tracker.key_found(&target_key); } else { // Try to trace to a deeper leaf @@ -498,15 +501,32 @@ fn process_branch_result( } result.metrics.total_elements_seen += branch_result.elements.len(); + Ok(()) } -/// Extract balance from a GroveDB Element. -/// -/// The address funds tree stores balances as items with sum items. -fn extract_balance_from_element(element: &Element) -> u64 { - match element { - Element::ItemWithSumItem(_, value, _) => *value as u64, - _ => 0, +impl TryFrom<&Element> for AddressFunds { + type Error = Error; + + /// Convert a GroveDB element into address funds (nonce and balance). + /// + /// The address funds tree stores the nonce as the item value and the balance as the sum item. + fn try_from(element: &Element) -> Result { + if let Element::ItemWithSumItem(nonce_bytes, balance, _) = element { + let nonce_bytes: [u8; 4] = nonce_bytes.as_slice().try_into().map_err(|_| { + Error::InvalidProvedResponse( + "address funds nonce must be exactly 4 bytes".to_string(), + ) + })?; + let nonce = AddressNonce::from_be_bytes(nonce_bytes); + let balance: u64 = (*balance).try_into().map_err(|_| { + Error::InvalidProvedResponse("address funds balance must fit into u64".to_string()) + })?; + return Ok(AddressFunds { nonce, balance }); + } + + Err(Error::InvalidProvedResponse( + "unexpected element type for address funds".to_string(), + )) } } @@ -525,7 +545,7 @@ impl Sdk { /// - `config`: Optional configuration; uses defaults if `None`. /// /// # Returns - /// - `Ok(AddressSyncResult)`: Contains found addresses with balances and absent addresses. + /// - `Ok(AddressSyncResult)`: Contains found addresses with balances/nonces and absent addresses. /// - `Err(Error)`: If the sync fails after exhausting retries. /// /// # Example @@ -564,11 +584,14 @@ mod tests { use super::*; #[test] - fn test_extract_balance() { - let item_with_sum_item = Element::ItemWithSumItem(vec![], 1000, None); - assert_eq!(extract_balance_from_element(&item_with_sum_item), 1000); + fn test_extract_funds_from_element() { + let item_with_sum_item = Element::ItemWithSumItem(vec![0, 0, 0, 5], 1000, None); + let funds = AddressFunds::try_from(&item_with_sum_item).expect("valid funds element"); + assert_eq!(funds.balance, 1000); + assert_eq!(funds.nonce, 5); let item = Element::Item(vec![1, 2, 3], None); - assert_eq!(extract_balance_from_element(&item), 0); + let err = AddressFunds::try_from(&item).unwrap_err(); + assert!(matches!(err, Error::InvalidProvedResponse(_))); } } diff --git a/packages/rs-sdk/src/platform/address_sync/provider.rs b/packages/rs-sdk/src/platform/address_sync/provider.rs index 2026cd22095..a961bbb5713 100644 --- a/packages/rs-sdk/src/platform/address_sync/provider.rs +++ b/packages/rs-sdk/src/platform/address_sync/provider.rs @@ -1,6 +1,6 @@ //! Address provider trait for address synchronization. -use super::types::{AddressIndex, AddressKey}; +use super::types::{AddressFunds, AddressIndex, AddressKey}; /// Trait for providing addresses to be synchronized. /// @@ -36,9 +36,9 @@ use super::types::{AddressIndex, AddressKey}; /// self.pending.clone() /// } /// -/// fn on_address_found(&mut self, index: AddressIndex, _key: &[u8], balance: u64) { +/// fn on_address_found(&mut self, index: AddressIndex, _key: &[u8], funds: AddressFunds) { /// // Update highest used and extend pending if needed -/// if balance > 0 { +/// if funds.balance > 0 { /// let new_end = index + self.gap_limit + 1; /// // Add new indices to pending... /// } @@ -75,8 +75,8 @@ pub trait AddressProvider: Send { /// # Arguments /// - `index`: The address index that was found /// - `key`: The address key bytes - /// - `balance`: The credits balance at this address - fn on_address_found(&mut self, index: AddressIndex, key: &[u8], balance: u64); + /// - `funds`: The nonce and credits balance at this address + fn on_address_found(&mut self, index: AddressIndex, key: &[u8], funds: AddressFunds); /// Called when an address is proven absent from the tree. /// diff --git a/packages/rs-sdk/src/platform/address_sync/types.rs b/packages/rs-sdk/src/platform/address_sync/types.rs index 9d8582c8ef7..99592833e18 100644 --- a/packages/rs-sdk/src/platform/address_sync/types.rs +++ b/packages/rs-sdk/src/platform/address_sync/types.rs @@ -1,6 +1,7 @@ //! Types for address synchronization. use dpp::fee::Credits; +use dpp::prelude::AddressNonce; use rs_dapi_client::RequestSettings; use std::collections::{BTreeMap, BTreeSet}; @@ -16,6 +17,14 @@ pub type AddressIndex = u32; /// Target keys that fall within this subtree's range need a branch query to resolve. pub type LeafBoundaryKey = Vec; +/// Funds stored for a platform address. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AddressFunds { + /// Address nonce used for anti-replay. + pub nonce: AddressNonce, + /// Credits balance held by the address. + pub balance: Credits, +} /// Configuration for address synchronization. #[derive(Debug, Clone)] pub struct AddressSyncConfig { @@ -62,10 +71,10 @@ impl Default for AddressSyncConfig { /// Result of address synchronization. #[derive(Debug)] pub struct AddressSyncResult { - /// Addresses found with their balances. + /// Addresses found with their balances and nonces. /// - /// Map of `(index, key)` to credits balance. - pub found: BTreeMap<(AddressIndex, AddressKey), Credits>, + /// Map of `(index, key)` to address funds. + pub found: BTreeMap<(AddressIndex, AddressKey), AddressFunds>, /// Addresses proven absent from the tree. /// @@ -101,12 +110,15 @@ impl AddressSyncResult { /// Get total credits across all found addresses. pub fn total_balance(&self) -> u64 { - self.found.values().sum() + self.found.values().map(|funds| funds.balance).sum() } /// Get count of addresses found with non-zero balance. pub fn non_zero_count(&self) -> usize { - self.found.values().filter(|&&b| b > 0).count() + self.found + .values() + .filter(|funds| funds.balance > 0) + .count() } } From 3dd577d900a5710a2c3b5808e19258915c33d1ca Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Tue, 13 Jan 2026 12:34:52 +0100 Subject: [PATCH 08/10] chore: fix merge issues --- packages/rs-sdk/tests/fetch/address_sync.rs | 24 ++++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/rs-sdk/tests/fetch/address_sync.rs b/packages/rs-sdk/tests/fetch/address_sync.rs index 5ecf20f1d4b..f644b33767e 100644 --- a/packages/rs-sdk/tests/fetch/address_sync.rs +++ b/packages/rs-sdk/tests/fetch/address_sync.rs @@ -1,5 +1,5 @@ use dash_sdk::platform::address_sync::{ - sync_address_balances, AddressIndex, AddressKey, AddressProvider, + sync_address_balances, AddressFunds, AddressIndex, AddressKey, AddressProvider, }; use std::collections::{BTreeMap, BTreeSet}; @@ -15,7 +15,7 @@ use super::{ struct TestAddressProvider { gap_limit: AddressIndex, pending: BTreeMap, - found: BTreeMap<(AddressIndex, AddressKey), u64>, + found: BTreeMap<(AddressIndex, AddressKey), AddressFunds>, absent: BTreeSet<(AddressIndex, AddressKey)>, highest_found_index: Option, } @@ -44,8 +44,8 @@ impl AddressProvider for TestAddressProvider { .collect() } - fn on_address_found(&mut self, index: AddressIndex, key: &[u8], balance: u64) { - self.found.insert((index, key.to_vec()), balance); + fn on_address_found(&mut self, index: AddressIndex, key: &[u8], funds: AddressFunds) { + self.found.insert((index, key.to_vec()), funds); self.pending.remove(&index); self.highest_found_index = Some(self.highest_found_index.map_or(index, |v| v.max(index))); } @@ -86,12 +86,20 @@ async fn test_sync_address_balances() { assert_eq!(result.absent.len(), 1); assert_eq!( - result.found.get(&(0, key_1.clone())), - Some(&PLATFORM_ADDRESS_1_BALANCE) + result + .found + .get(&(0, key_1.clone())) + .expect("found address 0") + .balance, + PLATFORM_ADDRESS_1_BALANCE ); assert_eq!( - result.found.get(&(1, key_2.clone())), - Some(&PLATFORM_ADDRESS_2_BALANCE) + result + .found + .get(&(1, key_2.clone())) + .expect("found address 1") + .balance, + PLATFORM_ADDRESS_2_BALANCE ); assert!(result.absent.contains(&(2, key_unknown.clone()))); From 3608bb6f3e1dc60b5338452ff38ac9c2171cf7ca Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Tue, 13 Jan 2026 12:44:24 +0100 Subject: [PATCH 09/10] chore: add TODOs --- packages/wasm-dpp2/src/platform_address/input_output.rs | 1 + packages/wasm-sdk/src/state_transitions/addresses.rs | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/packages/wasm-dpp2/src/platform_address/input_output.rs b/packages/wasm-dpp2/src/platform_address/input_output.rs index be1aeabc597..b223d97a47d 100644 --- a/packages/wasm-dpp2/src/platform_address/input_output.rs +++ b/packages/wasm-dpp2/src/platform_address/input_output.rs @@ -82,6 +82,7 @@ impl PlatformAddressInputWasm { /// An output specifies a Platform address that will receive credits, /// along with an optional amount to receive. When amount is None, /// the system distributes funds automatically (used for asset lock funding). +// TODO: Add nonce; see [WasmSdk::identity_create_from_addresses] notes. #[wasm_bindgen(js_name = "PlatformAddressOutput")] #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/packages/wasm-sdk/src/state_transitions/addresses.rs b/packages/wasm-sdk/src/state_transitions/addresses.rs index 4d741202ee8..ad35ecf6587 100644 --- a/packages/wasm-sdk/src/state_transitions/addresses.rs +++ b/packages/wasm-sdk/src/state_transitions/addresses.rs @@ -913,6 +913,14 @@ impl WasmSdk { /// /// @param options - Creation options including identity, inputs, and signers /// @returns Promise resolving to result with created identity and updated address infos + /// + /// ## Unstable + /// + /// This function is planned to be changed to require address nonces in the options to avoid potential privacy leaks. + // TODO: This function should require address nonces in the `IdentityCreateFromAddressesOptionsJs` + // to aviod potential leak of address owner IP address. Currently, it fetches nonces internally which may expose address usage. + // We need to implement address sync mechanism ([`sync_address_balances`](crate::platform::Platform::sync_address_balances)) + // to allow users to update nonces in a privacy-preserving way before calling this function. #[wasm_bindgen(js_name = "identityCreateFromAddresses")] pub async fn identity_create_from_addresses( &self, From 8909a36bef62a3ca9ac3b07c9a3932b5175ac33b Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Tue, 13 Jan 2026 12:56:42 +0100 Subject: [PATCH 10/10] typo --- packages/wasm-sdk/src/state_transitions/addresses.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wasm-sdk/src/state_transitions/addresses.rs b/packages/wasm-sdk/src/state_transitions/addresses.rs index ad35ecf6587..8e497ed00c5 100644 --- a/packages/wasm-sdk/src/state_transitions/addresses.rs +++ b/packages/wasm-sdk/src/state_transitions/addresses.rs @@ -918,7 +918,7 @@ impl WasmSdk { /// /// This function is planned to be changed to require address nonces in the options to avoid potential privacy leaks. // TODO: This function should require address nonces in the `IdentityCreateFromAddressesOptionsJs` - // to aviod potential leak of address owner IP address. Currently, it fetches nonces internally which may expose address usage. + // to avoid potential leak of address owner IP address. Currently, it fetches nonces internally which may expose address usage. // We need to implement address sync mechanism ([`sync_address_balances`](crate::platform::Platform::sync_address_balances)) // to allow users to update nonces in a privacy-preserving way before calling this function. #[wasm_bindgen(js_name = "identityCreateFromAddresses")]