From f032d8fded1323f74e60f5f83862e4a37367e268 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 22 Apr 2026 04:59:29 +0800 Subject: [PATCH 1/5] feat(key-wallet): add DIP-13 identity authentication accounts (ECDSA + BLS) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add two new `AccountType` variants for DIP-13 sub-feature 0' (per-identity signing keys the user employs to sign Dash Platform state transitions): - `IdentityAuthenticationEcdsa { identity_index }` — key_type 0', backed by a regular `Account` (secp256k1). - `IdentityAuthenticationBls { identity_index }` — key_type 1', backed by `BLSAccount`, gated on `#[cfg(feature = "bls")]`. Both account types use the DIP-13 derivation path `m/9'/coin_type'/5'/0'/key_type'/identity_index'` with hardened children for individual keys (`.../identity_index'/key_index'`). Address pools use `AbsentHardened` since DIP-13 mandates hardened leaves. ### Wiring - `AccountCollection` gains `identity_authentication_ecdsa: BTreeMap` and (under `bls`) `identity_authentication_bls: BTreeMap`, keyed by `identity_index`. All collection methods (`new`, `insert`, `insert_bls_account`, `contains_account_type`, `account_of_type[_mut]`, `bls_account_of_type[_mut]`, `all_accounts[_mut]`, `count`, `is_empty`, `clear`) are updated. - `ManagedAccountCollection`, `ManagedAccountType`, `CoreAccountTypeMatch` mirror the new variants and are routed through the usual matchers. - `AccountTypeToCheck::IdentityAuthentication{Ecdsa,Bls}` variants are added so conversions from `ManagedAccountType`/`AccountType` stay total. Identity authentication accounts are **Platform-only**: they are deliberately absent from every `TransactionType` relevance set (`TransactionRouter::get_relevant_account_types`), and the `ManagedAccountCollection::check_account_type` arms return empty results. Address matching in `ManagedCoreAccount::check_transaction_for_match` returns `None` for these variants for the same reason. - `Wallet::add_bls_account` now accepts `IdentityAuthenticationBls` in addition to `ProviderOperatorKeys`. - Two new DIP-9 `IndexConstPath<5>` constants per network (`IDENTITY_AUTHENTICATION_{ECDSA,BLS}_PATH_{MAINNET,TESTNET}`) and the matching `DerivationPathReference::BlockchainIdentityAuthentication{Ecdsa,Bls}` variants. - `asset_lock_builder::resolve_funding_account` is intentionally left untouched — identity authentication accounts do not fund asset locks. - `WalletAccountCreationOptions` is unchanged. Identity authentication accounts are per-identity and come into existence when the user registers a Platform identity, not at wallet creation. Callers insert them post-hoc via `Wallet::add_account` (ECDSA) or `Wallet::add_bls_account` (BLS). ### FFI `FFIAccountType` gains `IdentityAuthenticationEcdsa = 16` and `IdentityAuthenticationBls = 17`; `to_account_type` / `from_account_type` route the `index` parameter as `identity_index`. `FFIAccountMatch` emission for `CoreAccountTypeMatch::IdentityAuthentication*` reports the identity index in `account_index` (these variants are never produced by the L1 transaction router, but the FFI matcher stays exhaustive). ### Tests New `identity_authentication_tests` module in `account_type.rs` covers: ECDSA and BLS mainnet/testnet/regtest path derivation, `index()` / `derivation_path_reference()` / `AccountTypeToCheck` round-trip, and end-to-end insert / `contains_account_type` / `account_of_type` / `bls_account_of_type` round-trips through `AccountCollection`. BLS tests are `#[cfg(feature = "bls")]`-gated. Existing `test_wrong_account_type_for_bls` message was updated for the broadened `insert_bls_account` validation. ### Serialization compatibility Adding enum variants is forward-incompatible for `bincode::Encode`/ `Decode` — wallet blobs serialized by earlier v0.42-dev builds will fail to decode after this change. This is acceptable given the unstable 0.x API per `CLAUDE.md`. Serde uses its default (externally tagged) representation, so new readers still decode old data identically and old readers will error cleanly on new variants they cannot name. Verified: `cargo build -p key-wallet --all-features`, `cargo test -p key-wallet --lib --all-features`, `cargo clippy -p key-wallet --all-features --all-targets -- -D warnings`, `cargo fmt -p key-wallet --check`, and downstream `key-wallet-ffi` / `key-wallet-manager` builds and lib tests. Co-Authored-By: Claude Opus 4.7 (1M context) --- key-wallet-ffi/src/account.rs | 15 +- key-wallet-ffi/src/address_pool.rs | 73 +++- key-wallet-ffi/src/managed_account.rs | 40 ++- key-wallet-ffi/src/types.rs | 147 +++++--- key-wallet-ffi/src/wallet.rs | 45 ++- key-wallet/src/account/account_collection.rs | 105 +++++- .../src/account/account_collection_test.rs | 5 +- key-wallet/src/account/account_type.rs | 319 ++++++++++++++++++ key-wallet/src/account/bls_account.rs | 5 + key-wallet/src/account/mod.rs | 5 + key-wallet/src/dip9.rs | 104 ++++++ .../managed_account_collection.rs | 91 +++++ .../managed_account/managed_account_type.rs | 101 ++++++ key-wallet/src/managed_account/mod.rs | 65 ++++ .../transaction_checking/account_checker.rs | 11 + .../transaction_router/mod.rs | 32 +- key-wallet/src/wallet/accounts.rs | 23 +- 17 files changed, 1114 insertions(+), 72 deletions(-) diff --git a/key-wallet-ffi/src/account.rs b/key-wallet-ffi/src/account.rs index 4fa847d87..1d1881c2f 100644 --- a/key-wallet-ffi/src/account.rs +++ b/key-wallet-ffi/src/account.rs @@ -89,7 +89,20 @@ pub unsafe extern "C" fn wallet_get_account( } let wallet = &*wallet; - let account_type_rust = account_type.to_account_type(account_index); + let account_type_rust = match account_type.to_account_type(account_index) { + Ok(t) => t, + Err(mut err) => { + let code = err.code; + let message = if err.message.is_null() { + "Invalid account type".to_string() + } else { + let msg = std::ffi::CStr::from_ptr(err.message).to_string_lossy().to_string(); + err.free_message(); + msg + }; + return FFIAccountResult::error(code, message); + } + }; match wallet.inner().accounts.account_of_type(account_type_rust) { Some(account) => { diff --git a/key-wallet-ffi/src/address_pool.rs b/key-wallet-ffi/src/address_pool.rs index 0effc9ba5..073f920d5 100644 --- a/key-wallet-ffi/src/address_pool.rs +++ b/key-wallet-ffi/src/address_pool.rs @@ -45,6 +45,12 @@ fn get_managed_account_by_type<'a>( collection.identity_topup_not_bound.as_ref() } AccountType::IdentityInvitation => collection.identity_invitation.as_ref(), + AccountType::IdentityAuthenticationEcdsa { + identity_index, + } => collection.identity_authentication_ecdsa.get(identity_index), + AccountType::IdentityAuthenticationBls { + identity_index, + } => collection.identity_authentication_bls.get(identity_index), AccountType::AssetLockAddressTopUp => collection.asset_lock_address_topup.as_ref(), AccountType::AssetLockShieldedAddressTopUp => { collection.asset_lock_shielded_address_topup.as_ref() @@ -98,6 +104,12 @@ fn get_managed_account_by_type_mut<'a>( collection.identity_topup_not_bound.as_mut() } AccountType::IdentityInvitation => collection.identity_invitation.as_mut(), + AccountType::IdentityAuthenticationEcdsa { + identity_index, + } => collection.identity_authentication_ecdsa.get_mut(identity_index), + AccountType::IdentityAuthenticationBls { + identity_index, + } => collection.identity_authentication_bls.get_mut(identity_index), AccountType::AssetLockAddressTopUp => collection.asset_lock_address_topup.as_mut(), AccountType::AssetLockShieldedAddressTopUp => { collection.asset_lock_shielded_address_topup.as_mut() @@ -298,7 +310,20 @@ pub unsafe extern "C" fn managed_wallet_get_address_pool_info( let wrapper = &*managed_wallet; let managed_wallet = wrapper.inner(); - let account_type_rust = account_type.to_account_type(account_index); + let account_type_rust = match account_type.to_account_type(account_index) { + Ok(t) => t, + Err(mut e) => { + let msg = if e.message.is_null() { + "Invalid account type".to_string() + } else { + let m = std::ffi::CStr::from_ptr(e.message).to_string_lossy().to_string(); + e.free_message(); + m + }; + FFIError::set_error(error, e.code, msg); + return false; + } + }; // Get the specific managed account let managed_account = @@ -404,7 +429,20 @@ pub unsafe extern "C" fn managed_wallet_set_gap_limit( let managed_wallet = (&mut *managed_wallet).inner_mut(); - let account_type_rust = account_type.to_account_type(account_index); + let account_type_rust = match account_type.to_account_type(account_index) { + Ok(t) => t, + Err(mut e) => { + let msg = if e.message.is_null() { + "Invalid account type".to_string() + } else { + let m = std::ffi::CStr::from_ptr(e.message).to_string_lossy().to_string(); + e.free_message(); + m + }; + FFIError::set_error(error, e.code, msg); + return false; + } + }; // Get the specific managed account let managed_account = @@ -501,7 +539,20 @@ pub unsafe extern "C" fn managed_wallet_generate_addresses_to_index( let managed_wallet = (&mut *managed_wallet).inner_mut(); let wallet = &*wallet; - let account_type_rust = account_type.to_account_type(account_index); + let account_type_rust = match account_type.to_account_type(account_index) { + Ok(t) => t, + Err(mut e) => { + let msg = if e.message.is_null() { + "Invalid account type".to_string() + } else { + let m = std::ffi::CStr::from_ptr(e.message).to_string_lossy().to_string(); + e.free_message(); + m + }; + FFIError::set_error(error, e.code, msg); + return false; + } + }; let account_type_to_check = match account_type_rust.try_into() { Ok(check_type) => check_type, @@ -746,6 +797,22 @@ pub unsafe extern "C" fn managed_wallet_mark_address_used( } } } + if !found { + for account in collection.identity_authentication_ecdsa.values_mut() { + if account.mark_address_used(&address) { + found = true; + break; + } + } + } + if !found { + for account in collection.identity_authentication_bls.values_mut() { + if account.mark_address_used(&address) { + found = true; + break; + } + } + } if !found { if let Some(account) = &mut collection.asset_lock_address_topup { if account.mark_address_used(&address) { diff --git a/key-wallet-ffi/src/managed_account.rs b/key-wallet-ffi/src/managed_account.rs index ab5f8de19..de77fc5fe 100644 --- a/key-wallet-ffi/src/managed_account.rs +++ b/key-wallet-ffi/src/managed_account.rs @@ -218,7 +218,25 @@ pub unsafe extern "C" fn managed_wallet_get_account( } let managed_wallet = &*managed_wallet_ptr; - let account_type_rust = account_type.to_account_type(account_index); + let account_type_rust = match account_type.to_account_type(account_index) { + Ok(t) => t, + Err(mut e) => { + let code = e.code; + let message = if e.message.is_null() { + "Invalid account type".to_string() + } else { + let m = std::ffi::CStr::from_ptr(e.message).to_string_lossy().to_string(); + e.free_message(); + m + }; + // `wallet_manager_get_managed_wallet_info` allocated `managed_wallet_ptr` + // above; the success path frees it via `managed_wallet_info_free` at the + // bottom of this function. Do the same here before bailing so the + // managed-wallet handle isn't leaked on invalid-account-type errors. + crate::managed_wallet::managed_wallet_info_free(managed_wallet_ptr); + return FFIManagedCoreAccountResult::error(code, message); + } + }; let result = { use key_wallet::account::StandardAccountType; @@ -247,6 +265,12 @@ pub unsafe extern "C" fn managed_wallet_get_account( managed_collection.identity_topup_not_bound.as_ref() } AccountType::IdentityInvitation => managed_collection.identity_invitation.as_ref(), + AccountType::IdentityAuthenticationEcdsa { + identity_index, + } => managed_collection.identity_authentication_ecdsa.get(&identity_index), + AccountType::IdentityAuthenticationBls { + identity_index, + } => managed_collection.identity_authentication_bls.get(&identity_index), AccountType::AssetLockAddressTopUp => { managed_collection.asset_lock_address_topup.as_ref() } @@ -564,6 +588,12 @@ pub unsafe extern "C" fn managed_core_account_get_account_type( FFIAccountType::IdentityTopUpNotBoundToIdentity } AccountType::IdentityInvitation => FFIAccountType::IdentityInvitation, + AccountType::IdentityAuthenticationEcdsa { + .. + } => FFIAccountType::IdentityAuthenticationEcdsa, + AccountType::IdentityAuthenticationBls { + .. + } => FFIAccountType::IdentityAuthenticationBls, AccountType::AssetLockAddressTopUp => FFIAccountType::AssetLockAddressTopUp, AccountType::AssetLockShieldedAddressTopUp => FFIAccountType::AssetLockShieldedAddressTopUp, AccountType::ProviderVotingKeys => FFIAccountType::ProviderVotingKeys, @@ -1167,6 +1197,14 @@ pub unsafe extern "C" fn managed_core_account_get_address_pool( addresses, .. } => addresses, + ManagedAccountType::IdentityAuthenticationEcdsa { + addresses, + .. + } => addresses, + ManagedAccountType::IdentityAuthenticationBls { + addresses, + .. + } => addresses, }; let ffi_pool = FFIAddressPool { diff --git a/key-wallet-ffi/src/types.rs b/key-wallet-ffi/src/types.rs index 314e5b69c..20e5c7c5d 100644 --- a/key-wallet-ffi/src/types.rs +++ b/key-wallet-ffi/src/types.rs @@ -1,5 +1,6 @@ //! Common types for FFI interface +use crate::error::{FFIError, FFIErrorCode}; use dashcore::ephemerealdata::instant_lock::InstantLock; use dashcore::hashes::Hash; use key_wallet::managed_account::transaction_record::{OutputRole, TransactionDirection}; @@ -235,14 +236,24 @@ pub enum FFIAccountType { AssetLockAddressTopUp = 14, /// Asset lock shielded address top-up funding (subfeature 5) AssetLockShieldedAddressTopUp = 15, + /// Per-identity ECDSA authentication keys (DIP-13, sub-feature 0', key type 0'). + /// Path prefix: `m/9'/coin_type'/5'/0'/0'/identity_index'`. + IdentityAuthenticationEcdsa = 16, + /// Per-identity BLS authentication keys (DIP-13, sub-feature 0', key type 1'). + /// Path prefix: `m/9'/coin_type'/5'/0'/1'/identity_index'`. + IdentityAuthenticationBls = 17, } impl FFIAccountType { /// Convert to AccountType with the provided index (used where applicable). /// For types needing an index (e.g., IdentityTopUp.registration_index), the provided index is used. - pub fn to_account_type(self, index: u32) -> key_wallet::AccountType { + /// + /// Returns `Err` for variants that cannot be represented with a single `u32` index + /// (DashpayReceivingFunds, DashpayExternalAccount, PlatformPayment), since those + /// require additional data not carried by this conversion signature. + pub fn to_account_type(self, index: u32) -> Result { use key_wallet::account::account_type::StandardAccountType; - match self { + Ok(match self { FFIAccountType::StandardBIP44 => key_wallet::AccountType::Standard { index, standard_account_type: StandardAccountType::BIP44Account, @@ -273,42 +284,50 @@ impl FFIAccountType { FFIAccountType::ProviderOwnerKeys => key_wallet::AccountType::ProviderOwnerKeys, FFIAccountType::ProviderOperatorKeys => key_wallet::AccountType::ProviderOperatorKeys, FFIAccountType::ProviderPlatformKeys => key_wallet::AccountType::ProviderPlatformKeys, - // DashPay variants require additional identity IDs (user_identity_id and friend_identity_id) - // that are not part of the current FFI API. These types cannot be constructed via this - // conversion path. Attempting to use them is a programming error. - // - // TODO: Extend the FFI API to accept identity IDs for DashPay account creation: - // - Add new FFI functions like: - // * ffi_account_type_to_dashpay_receiving_funds(index, user_id[32], friend_id[32]) - // * ffi_account_type_to_dashpay_external_account(index, user_id[32], friend_id[32]) - // - Or extend to_account_type to accept optional identity ID parameters - // - // Until then, attempting to convert these variants will panic to prevent silent misrouting. + // DIP-13 authentication accounts use the provided `index` as + // `identity_index` (the hardened child at path level 6). + FFIAccountType::IdentityAuthenticationEcdsa => { + key_wallet::AccountType::IdentityAuthenticationEcdsa { + identity_index: index, + } + } + FFIAccountType::IdentityAuthenticationBls => { + key_wallet::AccountType::IdentityAuthenticationBls { + identity_index: index, + } + } + // DashPay and PlatformPayment variants require additional data (identity IDs + // or account/key_class indices) that are not part of this single-u32 + // conversion. Callers must use the dedicated FFI entry points (e.g. + // `wallet_add_dashpay_receiving_account`, `wallet_add_platform_payment_account`). FFIAccountType::DashpayReceivingFunds => { - panic!( + return Err(FFIError::error( + FFIErrorCode::InvalidInput, "FFIAccountType::DashpayReceivingFunds cannot be converted to AccountType \ - without user_identity_id and friend_identity_id. The FFI API does not yet \ - support passing these 32-byte identity IDs. This is a programming error - \ - DashPay account creation must use a different API path." - ); + without user_identity_id and friend_identity_id. Use \ + wallet_add_dashpay_receiving_account() instead." + .to_string(), + )); } FFIAccountType::DashpayExternalAccount => { - panic!( + return Err(FFIError::error( + FFIErrorCode::InvalidInput, "FFIAccountType::DashpayExternalAccount cannot be converted to AccountType \ - without user_identity_id and friend_identity_id. The FFI API does not yet \ - support passing these 32-byte identity IDs. This is a programming error - \ - DashPay account creation must use a different API path." - ); + without user_identity_id and friend_identity_id. Use \ + wallet_add_dashpay_external_account_with_xpub_bytes() instead." + .to_string(), + )); } FFIAccountType::PlatformPayment => { - panic!( + return Err(FFIError::error( + FFIErrorCode::InvalidInput, "FFIAccountType::PlatformPayment cannot be converted to AccountType \ - without account and key_class indices. The FFI API does not yet \ - support passing these values. This is a programming error - \ - Platform Payment account creation must use a different API path." - ); + without account and key_class indices. Use \ + wallet_add_platform_payment_account() instead." + .to_string(), + )); } - } + }) } /// Convert from AccountType to FFI representation @@ -366,6 +385,12 @@ impl FFIAccountType { key_wallet::AccountType::ProviderPlatformKeys => { (FFIAccountType::ProviderPlatformKeys, 0, None) } + key_wallet::AccountType::IdentityAuthenticationEcdsa { + identity_index, + } => (FFIAccountType::IdentityAuthenticationEcdsa, *identity_index, None), + key_wallet::AccountType::IdentityAuthenticationBls { + identity_index, + } => (FFIAccountType::IdentityAuthenticationBls, *identity_index, None), key_wallet::AccountType::DashpayReceivingFunds { index, user_identity_id, @@ -641,7 +666,13 @@ impl FFIWalletAccountCreationOptions { } } - // Convert special account types if provided + // Convert special account types if provided. + // + // Variants that cannot be represented with a single `u32` index + // (DashpayReceivingFunds, DashpayExternalAccount, PlatformPayment) + // are silently skipped here because callers must use the dedicated + // entry points (e.g. `wallet_add_dashpay_receiving_account`). This + // conversion path has no error-return channel. let special_accounts = if !self.special_account_types.is_null() && self.special_account_types_count > 0 { @@ -651,7 +682,16 @@ impl FFIWalletAccountCreationOptions { ); let mut accounts = Vec::new(); for &ffi_type in slice { - accounts.push(ffi_type.to_account_type(0)); + match ffi_type.to_account_type(0) { + Ok(account_type) => accounts.push(account_type), + Err(mut err) => { + // Free the error message allocated by + // FFIError::error to avoid a leak since we + // cannot propagate the error from this + // signature. + err.free_message(); + } + } } Some(accounts) } else { @@ -910,24 +950,39 @@ mod tests { } #[test] - #[should_panic(expected = "DashpayReceivingFunds cannot be converted to AccountType")] - fn test_dashpay_receiving_funds_to_account_type_panics() { - // This should panic because we cannot construct a DashPay account without identity IDs - let _ = FFIAccountType::DashpayReceivingFunds.to_account_type(0); + fn test_dashpay_receiving_funds_to_account_type_returns_err() { + // Cannot construct a DashPay account without identity IDs + let result = FFIAccountType::DashpayReceivingFunds.to_account_type(0); + let err = result.expect_err("should be an error"); + assert_eq!(err.code, FFIErrorCode::InvalidInput); + unsafe { + let mut err = err; + err.free_message(); + } } #[test] - #[should_panic(expected = "DashpayExternalAccount cannot be converted to AccountType")] - fn test_dashpay_external_account_to_account_type_panics() { - // This should panic because we cannot construct a DashPay account without identity IDs - let _ = FFIAccountType::DashpayExternalAccount.to_account_type(0); + fn test_dashpay_external_account_to_account_type_returns_err() { + // Cannot construct a DashPay account without identity IDs + let result = FFIAccountType::DashpayExternalAccount.to_account_type(0); + let err = result.expect_err("should be an error"); + assert_eq!(err.code, FFIErrorCode::InvalidInput); + unsafe { + let mut err = err; + err.free_message(); + } } #[test] - #[should_panic(expected = "PlatformPayment cannot be converted to AccountType")] - fn test_platform_payment_to_account_type_panics() { - // This should panic because we cannot construct a Platform Payment account without indices - let _ = FFIAccountType::PlatformPayment.to_account_type(0); + fn test_platform_payment_to_account_type_returns_err() { + // Cannot construct a Platform Payment account without account/key_class indices + let result = FFIAccountType::PlatformPayment.to_account_type(0); + let err = result.expect_err("should be an error"); + assert_eq!(err.code, FFIErrorCode::InvalidInput); + unsafe { + let mut err = err; + err.free_message(); + } } #[test] @@ -957,7 +1012,8 @@ mod tests { #[test] fn test_non_dashpay_conversions_work() { // Verify that non-DashPay types still convert correctly - let standard_bip44 = FFIAccountType::StandardBIP44.to_account_type(5); + let standard_bip44 = + FFIAccountType::StandardBIP44.to_account_type(5).expect("StandardBIP44 should convert"); assert!(matches!( standard_bip44, key_wallet::AccountType::Standard { @@ -966,7 +1022,8 @@ mod tests { } )); - let coinjoin = FFIAccountType::CoinJoin.to_account_type(3); + let coinjoin = + FFIAccountType::CoinJoin.to_account_type(3).expect("CoinJoin should convert"); assert!(matches!( coinjoin, key_wallet::AccountType::CoinJoin { diff --git a/key-wallet-ffi/src/wallet.rs b/key-wallet-ffi/src/wallet.rs index 8eb9deb5b..c4bb3fd79 100644 --- a/key-wallet-ffi/src/wallet.rs +++ b/key-wallet-ffi/src/wallet.rs @@ -517,7 +517,20 @@ pub unsafe extern "C" fn wallet_add_account( let wallet = &mut *wallet; - let account_type_rust = account_type.to_account_type(account_index); + let account_type_rust = match account_type.to_account_type(account_index) { + Ok(t) => t, + Err(mut e) => { + let code = e.code; + let message = if e.message.is_null() { + "Invalid account type".to_string() + } else { + let m = std::ffi::CStr::from_ptr(e.message).to_string_lossy().to_string(); + e.free_message(); + m + }; + return crate::types::FFIAccountResult::error(code, message); + } + }; match wallet.inner_mut() { Some(w) => { @@ -746,7 +759,20 @@ pub unsafe extern "C" fn wallet_add_account_with_xpub_bytes( use key_wallet::ExtendedPubKey; - let account_type_rust = account_type.to_account_type(account_index); + let account_type_rust = match account_type.to_account_type(account_index) { + Ok(t) => t, + Err(mut e) => { + let code = e.code; + let message = if e.message.is_null() { + "Invalid account type".to_string() + } else { + let m = std::ffi::CStr::from_ptr(e.message).to_string_lossy().to_string(); + e.free_message(); + m + }; + return crate::types::FFIAccountResult::error(code, message); + } + }; // Parse the xpub from bytes (assuming it's a string representation) let xpub_slice = slice::from_raw_parts(xpub_bytes, xpub_len); @@ -869,7 +895,20 @@ pub unsafe extern "C" fn wallet_add_account_with_string_xpub( use key_wallet::ExtendedPubKey; - let account_type_rust = account_type.to_account_type(account_index); + let account_type_rust = match account_type.to_account_type(account_index) { + Ok(t) => t, + Err(mut e) => { + let code = e.code; + let message = if e.message.is_null() { + "Invalid account type".to_string() + } else { + let m = std::ffi::CStr::from_ptr(e.message).to_string_lossy().to_string(); + e.free_message(); + m + }; + return crate::types::FFIAccountResult::error(code, message); + } + }; // Parse the xpub from C string let xpub_str = match CStr::from_ptr(xpub_string).to_str() { diff --git a/key-wallet/src/account/account_collection.rs b/key-wallet/src/account/account_collection.rs index f430a13e8..eb0f13db2 100644 --- a/key-wallet/src/account/account_collection.rs +++ b/key-wallet/src/account/account_collection.rs @@ -58,6 +58,14 @@ pub struct AccountCollection { pub identity_topup_not_bound: Option, /// Identity invitation account (optional) pub identity_invitation: Option, + /// Per-identity ECDSA authentication accounts (DIP-13, sub-feature 0', + /// key type 0'), keyed by `identity_index`. Platform-only — these carry no + /// L1 UTXOs. + pub identity_authentication_ecdsa: BTreeMap, + /// Per-identity BLS authentication accounts (DIP-13, sub-feature 0', + /// key type 1'), keyed by `identity_index`. Platform-only. + #[cfg(feature = "bls")] + pub identity_authentication_bls: BTreeMap, /// Asset lock address top-up account (optional) pub asset_lock_address_topup: Option, /// Asset lock shielded address top-up account (optional) @@ -91,6 +99,9 @@ impl AccountCollection { identity_topup: BTreeMap::new(), identity_topup_not_bound: None, identity_invitation: None, + identity_authentication_ecdsa: BTreeMap::new(), + #[cfg(feature = "bls")] + identity_authentication_bls: BTreeMap::new(), asset_lock_address_topup: None, asset_lock_shielded_address_topup: None, provider_voting_keys: None, @@ -141,6 +152,18 @@ impl AccountCollection { AccountType::IdentityInvitation => { self.identity_invitation = Some(account); } + AccountType::IdentityAuthenticationEcdsa { + identity_index, + } => { + self.identity_authentication_ecdsa.insert(*identity_index, account); + } + AccountType::IdentityAuthenticationBls { + .. + } => { + return Err( + "IdentityAuthenticationBls requires BLSAccount, use insert_bls_account", + ); + } AccountType::AssetLockAddressTopUp => { self.asset_lock_address_topup = Some(account); } @@ -197,14 +220,29 @@ impl AccountCollection { Ok(()) } - /// Insert a BLS account for provider operator keys + /// Insert a BLS account for provider operator keys or identity + /// authentication. + /// + /// Accepts [`AccountType::ProviderOperatorKeys`] or + /// [`AccountType::IdentityAuthenticationBls`]. Rejects any other account + /// type. #[cfg(feature = "bls")] pub fn insert_bls_account(&mut self, account: BLSAccount) -> Result<(), &'static str> { - if !matches!(account.account_type, AccountType::ProviderOperatorKeys) { - return Err("BLS account must have ProviderOperatorKeys type"); + match account.account_type { + AccountType::ProviderOperatorKeys => { + self.provider_operator_keys = Some(account); + Ok(()) + } + AccountType::IdentityAuthenticationBls { + identity_index, + } => { + self.identity_authentication_bls.insert(identity_index, account); + Ok(()) + } + _ => { + Err("BLS account must have ProviderOperatorKeys or IdentityAuthenticationBls type") + } } - self.provider_operator_keys = Some(account); - Ok(()) } /// Insert an EdDSA account for provider platform keys @@ -242,6 +280,17 @@ impl AccountCollection { } => self.identity_topup.contains_key(registration_index), AccountType::IdentityTopUpNotBoundToIdentity => self.identity_topup_not_bound.is_some(), AccountType::IdentityInvitation => self.identity_invitation.is_some(), + AccountType::IdentityAuthenticationEcdsa { + identity_index, + } => self.identity_authentication_ecdsa.contains_key(identity_index), + #[cfg(feature = "bls")] + AccountType::IdentityAuthenticationBls { + identity_index, + } => self.identity_authentication_bls.contains_key(identity_index), + #[cfg(not(feature = "bls"))] + AccountType::IdentityAuthenticationBls { + .. + } => false, AccountType::AssetLockAddressTopUp => self.asset_lock_address_topup.is_some(), AccountType::AssetLockShieldedAddressTopUp => { self.asset_lock_shielded_address_topup.is_some() @@ -315,6 +364,12 @@ impl AccountCollection { } => self.identity_topup.get(®istration_index), AccountType::IdentityTopUpNotBoundToIdentity => self.identity_topup_not_bound.as_ref(), AccountType::IdentityInvitation => self.identity_invitation.as_ref(), + AccountType::IdentityAuthenticationEcdsa { + identity_index, + } => self.identity_authentication_ecdsa.get(&identity_index), + AccountType::IdentityAuthenticationBls { + .. + } => None, // BLSAccount, use bls_account_of_type AccountType::AssetLockAddressTopUp => self.asset_lock_address_topup.as_ref(), AccountType::AssetLockShieldedAddressTopUp => { self.asset_lock_shielded_address_topup.as_ref() @@ -382,6 +437,12 @@ impl AccountCollection { } => self.identity_topup.get_mut(®istration_index), AccountType::IdentityTopUpNotBoundToIdentity => self.identity_topup_not_bound.as_mut(), AccountType::IdentityInvitation => self.identity_invitation.as_mut(), + AccountType::IdentityAuthenticationEcdsa { + identity_index, + } => self.identity_authentication_ecdsa.get_mut(&identity_index), + AccountType::IdentityAuthenticationBls { + .. + } => None, // BLSAccount, use bls_account_of_type_mut AccountType::AssetLockAddressTopUp => self.asset_lock_address_topup.as_mut(), AccountType::AssetLockShieldedAddressTopUp => { self.asset_lock_shielded_address_topup.as_mut() @@ -449,6 +510,8 @@ impl AccountCollection { accounts.push(account); } + accounts.extend(self.identity_authentication_ecdsa.values()); + if let Some(account) = &self.asset_lock_address_topup { accounts.push(account); } @@ -497,6 +560,8 @@ impl AccountCollection { accounts.push(account); } + accounts.extend(self.identity_authentication_ecdsa.values_mut()); + if let Some(account) = &mut self.asset_lock_address_topup { accounts.push(account); } @@ -523,16 +588,27 @@ impl AccountCollection { accounts } - /// Get the BLS account (provider operator keys) + /// Get a BLS account by type. + /// + /// Supports [`AccountType::ProviderOperatorKeys`] and + /// [`AccountType::IdentityAuthenticationBls`] — returns `None` for other + /// types. #[cfg(feature = "bls")] pub fn bls_account_of_type(&self, account_type: AccountType) -> Option<&BLSAccount> { match account_type { AccountType::ProviderOperatorKeys => self.provider_operator_keys.as_ref(), + AccountType::IdentityAuthenticationBls { + identity_index, + } => self.identity_authentication_bls.get(&identity_index), _ => None, } } - /// Get the BLS account mutably (provider operator keys) + /// Get a BLS account by type mutably. + /// + /// Supports [`AccountType::ProviderOperatorKeys`] and + /// [`AccountType::IdentityAuthenticationBls`] — returns `None` for other + /// types. #[cfg(feature = "bls")] pub fn bls_account_of_type_mut( &mut self, @@ -540,6 +616,9 @@ impl AccountCollection { ) -> Option<&mut BLSAccount> { match account_type { AccountType::ProviderOperatorKeys => self.provider_operator_keys.as_mut(), + AccountType::IdentityAuthenticationBls { + identity_index, + } => self.identity_authentication_bls.get_mut(&identity_index), _ => None, } } @@ -575,6 +654,11 @@ impl AccountCollection { count += 1; } + #[cfg(feature = "bls")] + { + count += self.identity_authentication_bls.len(); + } + #[cfg(feature = "eddsa")] if self.provider_platform_keys.is_some() { count += 1; @@ -605,6 +689,7 @@ impl AccountCollection { && self.identity_topup.is_empty() && self.identity_topup_not_bound.is_none() && self.identity_invitation.is_none() + && self.identity_authentication_ecdsa.is_empty() && self.asset_lock_address_topup.is_none() && self.asset_lock_shielded_address_topup.is_none() && self.provider_voting_keys.is_none() @@ -612,7 +697,9 @@ impl AccountCollection { #[cfg(feature = "bls")] { - is_empty = is_empty && self.provider_operator_keys.is_none(); + is_empty = is_empty + && self.provider_operator_keys.is_none() + && self.identity_authentication_bls.is_empty(); } #[cfg(feature = "eddsa")] @@ -632,6 +719,7 @@ impl AccountCollection { self.identity_topup.clear(); self.identity_topup_not_bound = None; self.identity_invitation = None; + self.identity_authentication_ecdsa.clear(); self.asset_lock_address_topup = None; self.asset_lock_shielded_address_topup = None; self.provider_voting_keys = None; @@ -639,6 +727,7 @@ impl AccountCollection { #[cfg(feature = "bls")] { self.provider_operator_keys = None; + self.identity_authentication_bls.clear(); } #[cfg(feature = "eddsa")] { diff --git a/key-wallet/src/account/account_collection_test.rs b/key-wallet/src/account/account_collection_test.rs index dead953e0..813dc8a82 100644 --- a/key-wallet/src/account/account_collection_test.rs +++ b/key-wallet/src/account/account_collection_test.rs @@ -125,7 +125,10 @@ mod tests { let result = collection.insert_bls_account(bls_account); assert!(result.is_err()); - assert_eq!(result.unwrap_err(), "BLS account must have ProviderOperatorKeys type"); + assert_eq!( + result.unwrap_err(), + "BLS account must have ProviderOperatorKeys or IdentityAuthenticationBls type" + ); } #[test] diff --git a/key-wallet/src/account/account_type.rs b/key-wallet/src/account/account_type.rs index ba1830e2d..712393e26 100644 --- a/key-wallet/src/account/account_type.rs +++ b/key-wallet/src/account/account_type.rs @@ -53,6 +53,32 @@ pub enum AccountType { IdentityTopUpNotBoundToIdentity, /// Identity invitation funding IdentityInvitation, + /// Per-identity authentication keys using ECDSA (DIP-13, sub-feature 0', key type 0'). + /// + /// Account-level path: `m/9'/coin_type'/5'/0'/0'/identity_index'`. Individual + /// keys live at `.../identity_index'/key_index'` and are managed sequentially + /// by the `AddressPool` below this account. These accounts carry no L1 + /// balance — they are pure signing-key chains the user employs to sign Dash + /// Platform state transitions. + IdentityAuthenticationEcdsa { + /// Which identity in this wallet these keys belong to (hardened). + identity_index: u32, + }, + /// Per-identity authentication keys using BLS (DIP-13, sub-feature 0', key type 1'). + /// + /// Account-level path: `m/9'/coin_type'/5'/0'/1'/identity_index'`. When the + /// `bls` feature is enabled this is backed by + /// [`BLSAccount`](crate::account::BLSAccount); like + /// [`AccountType::IdentityAuthenticationEcdsa`] these carry no L1 balance. + /// + /// The variant itself is always present so that downstream pattern matches + /// remain exhaustive regardless of features; the BLS-typed storage it maps + /// into (see [`crate::account::AccountCollection::identity_authentication_bls`]) + /// is what is gated on the `bls` feature. + IdentityAuthenticationBls { + /// Which identity in this wallet these keys belong to (hardened). + identity_index: u32, + }, /// Asset lock address top-up funding (subfeature 4) /// Path: m/9'/coinType'/5'/4'/index' AssetLockAddressTopUp, @@ -125,6 +151,16 @@ impl TryFrom for AccountTypeToCheck { Ok(AccountTypeToCheck::IdentityTopUpNotBound) } AccountType::IdentityInvitation => Ok(AccountTypeToCheck::IdentityInvitation), + AccountType::IdentityAuthenticationEcdsa { + .. + } + | AccountType::IdentityAuthenticationBls { + .. + } => { + // DIP-13 per-identity authentication accounts are Platform-only, + // operating on Dash Platform rather than the Core chain. + Err(PlatformAccountConversionError) + } AccountType::AssetLockAddressTopUp => Ok(AccountTypeToCheck::AssetLockAddressTopUp), AccountType::AssetLockShieldedAddressTopUp => { Ok(AccountTypeToCheck::AssetLockShieldedAddressTopUp) @@ -180,6 +216,12 @@ impl AccountType { } | Self::IdentityTopUpNotBoundToIdentity | Self::IdentityInvitation + | Self::IdentityAuthenticationEcdsa { + .. + } + | Self::IdentityAuthenticationBls { + .. + } | Self::AssetLockAddressTopUp | Self::AssetLockShieldedAddressTopUp | Self::ProviderVotingKeys @@ -225,6 +267,12 @@ impl AccountType { Self::IdentityInvitation { .. } => DerivationPathReference::BlockchainIdentityCreditInvitationFunding, + Self::IdentityAuthenticationEcdsa { + .. + } => DerivationPathReference::BlockchainIdentityAuthenticationEcdsa, + Self::IdentityAuthenticationBls { + .. + } => DerivationPathReference::BlockchainIdentityAuthenticationBls, Self::AssetLockAddressTopUp { .. } => DerivationPathReference::BlockchainAssetLockAddressTopupFunding, @@ -352,6 +400,44 @@ impl AccountType { _ => Err(crate::error::Error::InvalidNetwork), } } + Self::IdentityAuthenticationEcdsa { + identity_index, + } => { + // DIP-13: m/9'/coin_type'/5'/0'/0'/identity_index' + // The base const supplies the first 5 hardened levels; we append identity_index'. + let base_path = match network { + Network::Mainnet => crate::dip9::IDENTITY_AUTHENTICATION_ECDSA_PATH_MAINNET, + Network::Testnet | Network::Devnet | Network::Regtest => { + crate::dip9::IDENTITY_AUTHENTICATION_ECDSA_PATH_TESTNET + } + _ => return Err(crate::error::Error::InvalidNetwork), + }; + let mut path = DerivationPath::from(base_path); + path.push( + ChildNumber::from_hardened_idx(*identity_index) + .map_err(crate::error::Error::Bip32)?, + ); + Ok(path) + } + Self::IdentityAuthenticationBls { + identity_index, + } => { + // DIP-13: m/9'/coin_type'/5'/0'/1'/identity_index' + // The base const supplies the first 5 hardened levels; we append identity_index'. + let base_path = match network { + Network::Mainnet => crate::dip9::IDENTITY_AUTHENTICATION_BLS_PATH_MAINNET, + Network::Testnet | Network::Devnet | Network::Regtest => { + crate::dip9::IDENTITY_AUTHENTICATION_BLS_PATH_TESTNET + } + _ => return Err(crate::error::Error::InvalidNetwork), + }; + let mut path = DerivationPath::from(base_path); + path.push( + ChildNumber::from_hardened_idx(*identity_index) + .map_err(crate::error::Error::Bip32)?, + ); + Ok(path) + } Self::AssetLockAddressTopUp => { // Base path without index - actual key index added when deriving match network { @@ -493,3 +579,236 @@ impl AccountType { } } } + +#[cfg(test)] +mod identity_authentication_tests { + use super::*; + + /// Helper: build a `DerivationPath` of hardened children from the given + /// indices. Keeps tests readable. + fn hardened_path(indices: &[u32]) -> DerivationPath { + DerivationPath::from( + indices.iter().map(|i| ChildNumber::from_hardened_idx(*i).unwrap()).collect::>(), + ) + } + + #[test] + fn test_identity_authentication_ecdsa_mainnet_path() { + let account_type = AccountType::IdentityAuthenticationEcdsa { + identity_index: 0, + }; + let path = account_type.derivation_path(Network::Mainnet).unwrap(); + // m/9'/5'/5'/0'/0'/0' + assert_eq!(path, hardened_path(&[9, 5, 5, 0, 0, 0])); + } + + #[test] + fn test_identity_authentication_ecdsa_testnet_path() { + let account_type = AccountType::IdentityAuthenticationEcdsa { + identity_index: 7, + }; + let path = account_type.derivation_path(Network::Testnet).unwrap(); + // m/9'/1'/5'/0'/0'/7' + assert_eq!(path, hardened_path(&[9, 1, 5, 0, 0, 7])); + } + + #[test] + fn test_identity_authentication_ecdsa_index_is_none() { + // `index()` is for BIP44-style account indices, not identity indices. + let account_type = AccountType::IdentityAuthenticationEcdsa { + identity_index: 42, + }; + assert!(account_type.index().is_none()); + } + + #[test] + fn test_identity_authentication_ecdsa_derivation_path_reference() { + let account_type = AccountType::IdentityAuthenticationEcdsa { + identity_index: 0, + }; + assert_eq!( + account_type.derivation_path_reference(), + crate::dip9::DerivationPathReference::BlockchainIdentityAuthenticationEcdsa, + ); + } + + #[test] + fn test_identity_authentication_ecdsa_to_account_type_to_check_errs() { + // DIP-13 identity-authentication accounts are Platform-only and must + // never be mapped onto a Core-chain [`AccountTypeToCheck`] variant. + let account_type = AccountType::IdentityAuthenticationEcdsa { + identity_index: 3, + }; + let result: Result = account_type.try_into(); + assert_eq!(result, Err(PlatformAccountConversionError)); + } + + #[test] + fn test_identity_authentication_bls_mainnet_path() { + let account_type = AccountType::IdentityAuthenticationBls { + identity_index: 0, + }; + let path = account_type.derivation_path(Network::Mainnet).unwrap(); + // m/9'/5'/5'/0'/1'/0' + assert_eq!(path, hardened_path(&[9, 5, 5, 0, 1, 0])); + } + + #[test] + fn test_identity_authentication_bls_testnet_path() { + let account_type = AccountType::IdentityAuthenticationBls { + identity_index: 2, + }; + let path = account_type.derivation_path(Network::Testnet).unwrap(); + // m/9'/1'/5'/0'/1'/2' + assert_eq!(path, hardened_path(&[9, 1, 5, 0, 1, 2])); + } + + #[test] + fn test_identity_authentication_bls_regtest_uses_testnet_coin_type() { + let account_type = AccountType::IdentityAuthenticationBls { + identity_index: 5, + }; + let path = account_type.derivation_path(Network::Regtest).unwrap(); + // Regtest/Devnet use the same coin_type (1') as Testnet. + assert_eq!(path, hardened_path(&[9, 1, 5, 0, 1, 5])); + } + + #[test] + fn test_identity_authentication_bls_index_is_none() { + let account_type = AccountType::IdentityAuthenticationBls { + identity_index: 99, + }; + assert!(account_type.index().is_none()); + } + + #[test] + fn test_identity_authentication_bls_derivation_path_reference() { + let account_type = AccountType::IdentityAuthenticationBls { + identity_index: 0, + }; + assert_eq!( + account_type.derivation_path_reference(), + crate::dip9::DerivationPathReference::BlockchainIdentityAuthenticationBls, + ); + } + + #[test] + fn test_identity_authentication_bls_to_account_type_to_check_errs() { + // DIP-13 identity-authentication accounts are Platform-only and must + // never be mapped onto a Core-chain [`AccountTypeToCheck`] variant. + let account_type = AccountType::IdentityAuthenticationBls { + identity_index: 3, + }; + let result: Result = account_type.try_into(); + assert_eq!(result, Err(PlatformAccountConversionError)); + } + + /// End-to-end: insert an ECDSA identity-authentication account into an + /// `AccountCollection` and round-trip through `contains_account_type` / + /// `account_of_type`. + #[test] + fn test_account_collection_round_trip_ecdsa() { + use crate::account::{Account, AccountCollection}; + use crate::bip32::{ExtendedPrivKey, ExtendedPubKey}; + use crate::mnemonic::{Language, Mnemonic}; + use secp256k1::Secp256k1; + + let mut collection = AccountCollection::new(); + + let mnemonic = Mnemonic::from_phrase( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon \ + abandon about", + Language::English, + ) + .unwrap(); + let seed = mnemonic.to_seed(""); + let master = ExtendedPrivKey::new_master(Network::Testnet, &seed).unwrap(); + let secp = Secp256k1::new(); + let xpub = ExtendedPubKey::from_priv(&secp, &master); + + let account_type = AccountType::IdentityAuthenticationEcdsa { + identity_index: 4, + }; + let account = Account::new(None, account_type, xpub, Network::Testnet).unwrap(); + + assert!(!collection.contains_account_type(&account_type)); + assert!(collection.insert(account).is_ok()); + assert!(collection.contains_account_type(&account_type)); + assert!(collection.account_of_type(account_type).is_some()); + + // Sanity: the ECDSA variant is *not* routed to the BLS map, so the BLS + // lookup on an ECDSA variant returns None regardless of features. + #[cfg(feature = "bls")] + { + let bls_probe = AccountType::IdentityAuthenticationBls { + identity_index: 4, + }; + assert!(collection.bls_account_of_type(bls_probe).is_none()); + } + } + + /// End-to-end: insert a BLS identity-authentication account into an + /// `AccountCollection` via `insert_bls_account`, and round-trip through + /// `contains_account_type` / `bls_account_of_type`. + #[cfg(feature = "bls")] + #[test] + fn test_account_collection_round_trip_bls() { + use crate::account::{AccountCollection, BLSAccount}; + + let mut collection = AccountCollection::new(); + + let account_type = AccountType::IdentityAuthenticationBls { + identity_index: 11, + }; + let bls_account = + BLSAccount::from_seed(None, account_type, [7u8; 32], Network::Testnet).unwrap(); + + assert!(!collection.contains_account_type(&account_type)); + assert!(collection.insert_bls_account(bls_account).is_ok()); + assert!(collection.contains_account_type(&account_type)); + assert!(collection.bls_account_of_type(account_type).is_some()); + + // ECDSA `account_of_type` should refuse BLS lookups via the plain + // accessor. + assert!(collection.account_of_type(account_type).is_none()); + } + + /// Inserting a BLS account via the plain `insert` path must fail with a + /// pointer to `insert_bls_account`. + #[cfg(feature = "bls")] + #[test] + fn test_account_collection_rejects_bls_via_plain_insert() { + use crate::account::{Account, AccountCollection}; + use crate::bip32::{ExtendedPrivKey, ExtendedPubKey}; + use crate::mnemonic::{Language, Mnemonic}; + use secp256k1::Secp256k1; + + let mut collection = AccountCollection::new(); + + let mnemonic = Mnemonic::from_phrase( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon \ + abandon about", + Language::English, + ) + .unwrap(); + let seed = mnemonic.to_seed(""); + let master = ExtendedPrivKey::new_master(Network::Testnet, &seed).unwrap(); + let secp = Secp256k1::new(); + let xpub = ExtendedPubKey::from_priv(&secp, &master); + + // Using an Account (ECDSA) constructor but with a BLS auth type is a + // programming error — insert() routes it to the BLS error arm. + let account = Account::new( + None, + AccountType::IdentityAuthenticationBls { + identity_index: 0, + }, + xpub, + Network::Testnet, + ) + .unwrap(); + + let err = collection.insert(account).unwrap_err(); + assert!(err.contains("insert_bls_account")); + } +} diff --git a/key-wallet/src/account/bls_account.rs b/key-wallet/src/account/bls_account.rs index 12a1c031d..445d984e4 100644 --- a/key-wallet/src/account/bls_account.rs +++ b/key-wallet/src/account/bls_account.rs @@ -247,6 +247,11 @@ impl } => Some(ChildNumber::Hardened { index: registration_index, }), + AccountType::IdentityAuthenticationBls { + identity_index, + } => Some(ChildNumber::Hardened { + index: identity_index, + }), _ => None, } } diff --git a/key-wallet/src/account/mod.rs b/key-wallet/src/account/mod.rs index 177a42747..dd5d0cea6 100644 --- a/key-wallet/src/account/mod.rs +++ b/key-wallet/src/account/mod.rs @@ -179,6 +179,11 @@ impl AccountDerivation Some(ChildNumber::Hardened { index: registration_index, }), + AccountType::IdentityAuthenticationEcdsa { + identity_index, + } => Some(ChildNumber::Hardened { + index: identity_index, + }), _ => None, } } diff --git a/key-wallet/src/dip9.rs b/key-wallet/src/dip9.rs index 276972df7..4c56dee8c 100644 --- a/key-wallet/src/dip9.rs +++ b/key-wallet/src/dip9.rs @@ -30,6 +30,12 @@ pub enum DerivationPathReference { PlatformPayment = 16, BlockchainAssetLockAddressTopupFunding = 17, BlockchainAssetLockShieldedAddressTopupFunding = 18, + /// Per-identity authentication keys using ECDSA (DIP-13, sub-feature 0', key type 0'). + /// Path prefix: m/9'/coin_type'/5'/0'/0'/identity_index' + BlockchainIdentityAuthenticationEcdsa = 19, + /// Per-identity authentication keys using BLS (DIP-13, sub-feature 0', key type 1'). + /// Path prefix: m/9'/coin_type'/5'/0'/1'/identity_index' + BlockchainIdentityAuthenticationBls = 20, Root = 255, } @@ -423,6 +429,104 @@ pub const ASSET_LOCK_SHIELDED_ADDRESS_TOPUP_PATH_TESTNET: IndexConstPath<4> = In path_type: DerivationPathType::CREDIT_FUNDING, }; +// DIP-13 Identity Authentication (ECDSA) paths — 5 hardened levels: +// m/9'/coin_type'/5'/0'/0' (key_type = 0' = ECDSA). +// The account level adds identity_index' on top (see `AccountType::derivation_path`). +pub const IDENTITY_AUTHENTICATION_ECDSA_PATH_MAINNET: IndexConstPath<5> = IndexConstPath { + indexes: [ + ChildNumber::Hardened { + index: FEATURE_PURPOSE, + }, + ChildNumber::Hardened { + index: DASH_COIN_TYPE, + }, + ChildNumber::Hardened { + index: FEATURE_PURPOSE_IDENTITIES, + }, + ChildNumber::Hardened { + index: FEATURE_PURPOSE_IDENTITIES_SUBFEATURE_AUTHENTICATION, + }, + ChildNumber::Hardened { + // key_type = 0' for ECDSA + index: 0, + }, + ], + reference: DerivationPathReference::BlockchainIdentityAuthenticationEcdsa, + path_type: DerivationPathType::SINGLE_USER_AUTHENTICATION, +}; + +pub const IDENTITY_AUTHENTICATION_ECDSA_PATH_TESTNET: IndexConstPath<5> = IndexConstPath { + indexes: [ + ChildNumber::Hardened { + index: FEATURE_PURPOSE, + }, + ChildNumber::Hardened { + index: DASH_TESTNET_COIN_TYPE, + }, + ChildNumber::Hardened { + index: FEATURE_PURPOSE_IDENTITIES, + }, + ChildNumber::Hardened { + index: FEATURE_PURPOSE_IDENTITIES_SUBFEATURE_AUTHENTICATION, + }, + ChildNumber::Hardened { + // key_type = 0' for ECDSA + index: 0, + }, + ], + reference: DerivationPathReference::BlockchainIdentityAuthenticationEcdsa, + path_type: DerivationPathType::SINGLE_USER_AUTHENTICATION, +}; + +// DIP-13 Identity Authentication (BLS) paths — 5 hardened levels: +// m/9'/coin_type'/5'/0'/1' (key_type = 1' = BLS). +// The account level adds identity_index' on top (see `AccountType::derivation_path`). +pub const IDENTITY_AUTHENTICATION_BLS_PATH_MAINNET: IndexConstPath<5> = IndexConstPath { + indexes: [ + ChildNumber::Hardened { + index: FEATURE_PURPOSE, + }, + ChildNumber::Hardened { + index: DASH_COIN_TYPE, + }, + ChildNumber::Hardened { + index: FEATURE_PURPOSE_IDENTITIES, + }, + ChildNumber::Hardened { + index: FEATURE_PURPOSE_IDENTITIES_SUBFEATURE_AUTHENTICATION, + }, + ChildNumber::Hardened { + // key_type = 1' for BLS + index: 1, + }, + ], + reference: DerivationPathReference::BlockchainIdentityAuthenticationBls, + path_type: DerivationPathType::SINGLE_USER_AUTHENTICATION, +}; + +pub const IDENTITY_AUTHENTICATION_BLS_PATH_TESTNET: IndexConstPath<5> = IndexConstPath { + indexes: [ + ChildNumber::Hardened { + index: FEATURE_PURPOSE, + }, + ChildNumber::Hardened { + index: DASH_TESTNET_COIN_TYPE, + }, + ChildNumber::Hardened { + index: FEATURE_PURPOSE_IDENTITIES, + }, + ChildNumber::Hardened { + index: FEATURE_PURPOSE_IDENTITIES_SUBFEATURE_AUTHENTICATION, + }, + ChildNumber::Hardened { + // key_type = 1' for BLS + index: 1, + }, + ], + reference: DerivationPathReference::BlockchainIdentityAuthenticationBls, + path_type: DerivationPathType::SINGLE_USER_AUTHENTICATION, +}; + // Authentication Keys Paths pub const IDENTITY_AUTHENTICATION_PATH_MAINNET: IndexConstPath<4> = IndexConstPath { indexes: [ diff --git a/key-wallet/src/managed_account/managed_account_collection.rs b/key-wallet/src/managed_account/managed_account_collection.rs index 10f5adcb4..045097661 100644 --- a/key-wallet/src/managed_account/managed_account_collection.rs +++ b/key-wallet/src/managed_account/managed_account_collection.rs @@ -126,6 +126,16 @@ pub struct ManagedAccountCollection { pub identity_topup_not_bound: Option, /// Identity invitation account (optional) pub identity_invitation: Option, + /// Per-identity ECDSA authentication accounts (DIP-13), keyed by + /// `identity_index`. Platform-only — carries no L1 UTXOs. + pub identity_authentication_ecdsa: BTreeMap, + /// Per-identity BLS authentication accounts (DIP-13), keyed by + /// `identity_index`. Platform-only. + /// + /// Storage type is `ManagedCoreAccount`, which is not BLS-dependent, so the + /// field is available regardless of the `bls` feature — mirroring how + /// `provider_operator_keys` is handled at the managed-account level. + pub identity_authentication_bls: BTreeMap, /// Asset lock address top-up account (optional) pub asset_lock_address_topup: Option, /// Asset lock shielded address top-up account (optional) @@ -158,6 +168,8 @@ impl ManagedAccountCollection { identity_topup: BTreeMap::new(), identity_topup_not_bound: None, identity_invitation: None, + identity_authentication_ecdsa: BTreeMap::new(), + identity_authentication_bls: BTreeMap::new(), asset_lock_address_topup: None, asset_lock_shielded_address_topup: None, provider_voting_keys: None, @@ -204,6 +216,14 @@ impl ManagedAccountCollection { ManagedAccountType::IdentityInvitation { .. } => self.identity_invitation.is_some(), + ManagedAccountType::IdentityAuthenticationEcdsa { + identity_index, + .. + } => self.identity_authentication_ecdsa.contains_key(identity_index), + ManagedAccountType::IdentityAuthenticationBls { + identity_index, + .. + } => self.identity_authentication_bls.contains_key(identity_index), ManagedAccountType::AssetLockAddressTopUp { .. } => self.asset_lock_address_topup.is_some(), @@ -309,6 +329,18 @@ impl ManagedAccountCollection { } => { self.identity_invitation = Some(account); } + ManagedAccountType::IdentityAuthenticationEcdsa { + identity_index, + .. + } => { + self.identity_authentication_ecdsa.insert(*identity_index, account); + } + ManagedAccountType::IdentityAuthenticationBls { + identity_index, + .. + } => { + self.identity_authentication_bls.insert(*identity_index, account); + } ManagedAccountType::AssetLockAddressTopUp { .. } => { @@ -438,6 +470,21 @@ impl ManagedAccountCollection { } } + // Convert per-identity ECDSA authentication accounts + for (index, account) in &account_collection.identity_authentication_ecdsa { + if let Ok(managed_account) = Self::create_managed_account_from_account(account) { + managed_collection.identity_authentication_ecdsa.insert(*index, managed_account); + } + } + + // Convert per-identity BLS authentication accounts + #[cfg(feature = "bls")] + for (index, account) in &account_collection.identity_authentication_bls { + if let Ok(managed_account) = Self::create_managed_account_from_bls_account(account) { + managed_collection.identity_authentication_bls.insert(*index, managed_account); + } + } + if let Some(account) = &account_collection.asset_lock_address_topup { if let Ok(managed_account) = Self::create_managed_account_from_account(account) { managed_collection.asset_lock_address_topup = Some(managed_account); @@ -665,6 +712,38 @@ impl ManagedAccountCollection { addresses, } } + AccountType::IdentityAuthenticationEcdsa { + identity_index, + } => { + // DIP-13: hardened leaves per the spec. + let addresses = AddressPool::new( + base_path, + AddressPoolType::AbsentHardened, + DEFAULT_SPECIAL_GAP_LIMIT, + network, + key_source, + )?; + ManagedAccountType::IdentityAuthenticationEcdsa { + identity_index, + addresses, + } + } + AccountType::IdentityAuthenticationBls { + identity_index, + } => { + // DIP-13: hardened leaves per the spec. + let addresses = AddressPool::new( + base_path, + AddressPoolType::AbsentHardened, + DEFAULT_SPECIAL_GAP_LIMIT, + network, + key_source, + )?; + ManagedAccountType::IdentityAuthenticationBls { + identity_index, + addresses, + } + } AccountType::AssetLockAddressTopUp => { let addresses = AddressPool::new( base_path, @@ -962,6 +1041,10 @@ impl ManagedAccountCollection { accounts.push(account); } + accounts.extend(self.identity_authentication_ecdsa.values()); + + accounts.extend(self.identity_authentication_bls.values()); + if let Some(account) = &self.asset_lock_address_topup { accounts.push(account); } @@ -1021,6 +1104,10 @@ impl ManagedAccountCollection { accounts.push(account); } + accounts.extend(self.identity_authentication_ecdsa.values_mut()); + + accounts.extend(self.identity_authentication_bls.values_mut()); + if let Some(account) = &mut self.asset_lock_address_topup { accounts.push(account); } @@ -1085,6 +1172,8 @@ impl ManagedAccountCollection { && self.identity_topup.is_empty() && self.identity_topup_not_bound.is_none() && self.identity_invitation.is_none() + && self.identity_authentication_ecdsa.is_empty() + && self.identity_authentication_bls.is_empty() && self.asset_lock_address_topup.is_none() && self.asset_lock_shielded_address_topup.is_none() && self.provider_voting_keys.is_none() @@ -1105,6 +1194,8 @@ impl ManagedAccountCollection { self.identity_topup.clear(); self.identity_topup_not_bound = None; self.identity_invitation = None; + self.identity_authentication_ecdsa.clear(); + self.identity_authentication_bls.clear(); self.asset_lock_address_topup = None; self.asset_lock_shielded_address_topup = None; self.provider_voting_keys = None; diff --git a/key-wallet/src/managed_account/managed_account_type.rs b/key-wallet/src/managed_account/managed_account_type.rs index d86c6812f..bb492585d 100644 --- a/key-wallet/src/managed_account/managed_account_type.rs +++ b/key-wallet/src/managed_account/managed_account_type.rs @@ -58,6 +58,27 @@ pub enum ManagedAccountType { /// Identity invitation address pool addresses: AddressPool, }, + /// Per-identity ECDSA authentication keys (DIP-13). + /// Path: `m/9'/coin_type'/5'/0'/0'/identity_index'`. Platform-only. + IdentityAuthenticationEcdsa { + /// Which identity in this wallet these keys belong to (hardened). + identity_index: u32, + /// Address pool for sequential key_index'. + addresses: AddressPool, + }, + /// Per-identity BLS authentication keys (DIP-13). + /// Path: `m/9'/coin_type'/5'/0'/1'/identity_index'`. Platform-only. + /// + /// The variant is always present regardless of the `bls` feature — only + /// `BLSAccount`-typed storage is cfg-gated. This keeps downstream pattern + /// matches on `ManagedAccountType` exhaustive in both feature + /// configurations. + IdentityAuthenticationBls { + /// Which identity in this wallet these keys belong to (hardened). + identity_index: u32, + /// Address pool for sequential key_index'. + addresses: AddressPool, + }, /// Asset lock address top-up funding (subfeature 4) /// Path: m/9'/coinType'/5'/4'/index' AssetLockAddressTopUp { @@ -155,6 +176,9 @@ impl ManagedAccountType { | Self::IdentityInvitation { .. } + | Self::IdentityAuthenticationEcdsa { + .. + } | Self::AssetLockAddressTopUp { .. } @@ -185,6 +209,9 @@ impl ManagedAccountType { account, .. } => Some(*account), + Self::IdentityAuthenticationBls { + .. + } => None, } } @@ -234,6 +261,10 @@ impl ManagedAccountType { addresses, .. } + | Self::IdentityAuthenticationEcdsa { + addresses, + .. + } | Self::AssetLockAddressTopUp { addresses, .. @@ -271,6 +302,10 @@ impl ManagedAccountType { | Self::PlatformPayment { addresses, .. + } + | Self::IdentityAuthenticationBls { + addresses, + .. } => vec![addresses], } } @@ -305,6 +340,10 @@ impl ManagedAccountType { addresses, .. } + | Self::IdentityAuthenticationEcdsa { + addresses, + .. + } | Self::AssetLockAddressTopUp { addresses, .. @@ -342,6 +381,10 @@ impl ManagedAccountType { | Self::PlatformPayment { addresses, .. + } + | Self::IdentityAuthenticationBls { + addresses, + .. } => vec![addresses], } } @@ -426,6 +469,18 @@ impl ManagedAccountType { Self::IdentityInvitation { .. } => AccountType::IdentityInvitation, + Self::IdentityAuthenticationEcdsa { + identity_index, + .. + } => AccountType::IdentityAuthenticationEcdsa { + identity_index: *identity_index, + }, + Self::IdentityAuthenticationBls { + identity_index, + .. + } => AccountType::IdentityAuthenticationBls { + identity_index: *identity_index, + }, Self::AssetLockAddressTopUp { .. } => AccountType::AssetLockAddressTopUp, @@ -607,6 +662,52 @@ impl ManagedAccountType { addresses: pool, }) } + AccountType::IdentityAuthenticationEcdsa { + identity_index, + } => { + // Identity authentication keys are signing/authentication + // material; falling back to the master path on a derivation + // error would produce a subtly-wrong pool at `m/`, so we + // propagate the error instead. + let path = account_type.derivation_path(network)?; + // Hardened leaves per DIP-13 — addresses must be generated from + // a private-key-bearing source (watch-only/no-key setups will + // create an empty pool that can still track derivation paths). + let pool = AddressPool::new( + path, + AddressPoolType::AbsentHardened, + DEFAULT_SPECIAL_GAP_LIMIT, + network, + key_source, + )?; + + Ok(Self::IdentityAuthenticationEcdsa { + identity_index, + addresses: pool, + }) + } + AccountType::IdentityAuthenticationBls { + identity_index, + } => { + // Identity authentication keys are signing/authentication + // material; falling back to the master path on a derivation + // error would produce a subtly-wrong pool at `m/`, so we + // propagate the error instead. + let path = account_type.derivation_path(network)?; + // Hardened leaves per DIP-13. + let pool = AddressPool::new( + path, + AddressPoolType::AbsentHardened, + DEFAULT_SPECIAL_GAP_LIMIT, + network, + key_source, + )?; + + Ok(Self::IdentityAuthenticationBls { + identity_index, + addresses: pool, + }) + } AccountType::AssetLockAddressTopUp => { let path = account_type .derivation_path(network) diff --git a/key-wallet/src/managed_account/mod.rs b/key-wallet/src/managed_account/mod.rs index c8064dae1..6eef7e29b 100644 --- a/key-wallet/src/managed_account/mod.rs +++ b/key-wallet/src/managed_account/mod.rs @@ -254,6 +254,10 @@ impl ManagedCoreAccount { addresses, .. } + | ManagedAccountType::IdentityAuthenticationEcdsa { + addresses, + .. + } | ManagedAccountType::AssetLockAddressTopUp { addresses, .. @@ -289,6 +293,10 @@ impl ManagedCoreAccount { | ManagedAccountType::PlatformPayment { addresses, .. + } + | ManagedAccountType::IdentityAuthenticationBls { + addresses, + .. } => { addresses.unused_addresses().first().and_then(|addr| addresses.address_index(addr)) } @@ -828,6 +836,10 @@ impl ManagedCoreAccount { | ManagedAccountType::PlatformPayment { addresses, .. + } + | ManagedAccountType::IdentityAuthenticationEcdsa { + addresses, + .. } => { // Create appropriate key source based on whether xpub is provided let key_source = match account_xpub { @@ -842,6 +854,25 @@ impl ManagedCoreAccount { _ => "Failed to generate address", }) } + ManagedAccountType::IdentityAuthenticationBls { + addresses, + .. + } => { + // `account_xpub` is an ECDSA extended pubkey and is useless for a + // BLS key pool. Callers that need to generate BLS identity-auth + // addresses must go through a dedicated BLS-aware API path + // (similar to `next_bls_operator_key` for ProviderOperatorKeys). + // Here we only allow progression when the pool already has a + // pre-derived address cached (NoKeySource). + addresses.next_unused(&address_pool::KeySource::NoKeySource, add_to_state).map_err( + |e| match e { + crate::error::Error::NoKeySource => { + "No unused addresses available and no key source provided" + } + _ => "Failed to generate address", + }, + ) + } ManagedAccountType::IdentityTopUp { addresses, .. @@ -925,6 +956,10 @@ impl ManagedCoreAccount { | ManagedAccountType::PlatformPayment { addresses, .. + } + | ManagedAccountType::IdentityAuthenticationEcdsa { + addresses, + .. } => { // Create appropriate key source based on whether xpub is provided let key_source = match account_xpub { @@ -939,6 +974,28 @@ impl ManagedCoreAccount { _ => "Failed to generate address with info", }) } + ManagedAccountType::IdentityAuthenticationBls { + addresses, + .. + } => { + // `account_xpub` is an ECDSA extended pubkey and is useless for a + // BLS key pool. Callers that need to generate BLS identity-auth + // addresses must go through a dedicated BLS-aware API path + // (similar to `next_bls_operator_key` for ProviderOperatorKeys). + // Here we only allow progression when the pool already has a + // pre-derived address cached (NoKeySource). + addresses + .next_unused_with_info( + &address_pool::KeySource::NoKeySource, + add_to_state, + ) + .map_err(|e| match e { + crate::error::Error::NoKeySource => { + "No unused addresses available and no key source provided" + } + _ => "Failed to generate address with info", + }) + } ManagedAccountType::IdentityTopUp { addresses, .. @@ -1252,6 +1309,14 @@ impl ManagedCoreAccount { | ManagedAccountType::PlatformPayment { addresses, .. + } + | ManagedAccountType::IdentityAuthenticationEcdsa { + addresses, + .. + } + | ManagedAccountType::IdentityAuthenticationBls { + addresses, + .. } => Some(addresses.gap_limit), } } diff --git a/key-wallet/src/transaction_checking/account_checker.rs b/key-wallet/src/transaction_checking/account_checker.rs index a9750f1aa..37241a714 100644 --- a/key-wallet/src/transaction_checking/account_checker.rs +++ b/key-wallet/src/transaction_checking/account_checker.rs @@ -760,6 +760,17 @@ impl ManagedCoreAccount { // They should never be checked for Core chain transactions. return None; } + ManagedAccountType::IdentityAuthenticationEcdsa { + .. + } + | ManagedAccountType::IdentityAuthenticationBls { + .. + } => { + // DIP-13 identity authentication keys are Platform-only — + // they never hold L1 UTXOs and should not be reported as + // relevant for Core chain transactions. + return None; + } }; Some(AccountMatch { diff --git a/key-wallet/src/transaction_checking/transaction_router/mod.rs b/key-wallet/src/transaction_checking/transaction_router/mod.rs index e26152648..e39df1678 100644 --- a/key-wallet/src/transaction_checking/transaction_router/mod.rs +++ b/key-wallet/src/transaction_checking/transaction_router/mod.rs @@ -169,8 +169,11 @@ impl TransactionRouter { /// Core account types that can be checked for transactions /// -/// Note: Platform Payment accounts (DIP-17) are NOT included here as they -/// operate on Dash Platform, not the Core chain. +/// Note: Platform Payment accounts (DIP-17) and DIP-13 identity authentication +/// accounts (sub-feature 0') are NOT included here — they operate on Dash +/// Platform, not the Core chain, so the conversions from +/// [`ManagedAccountType`]/[`crate::AccountType`] explicitly return +/// [`PlatformAccountConversionError`] for those variants. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AccountTypeToCheck { StandardBIP44, @@ -190,13 +193,14 @@ pub enum AccountTypeToCheck { DashpayExternalAccount, } -/// Error returned when trying to convert a Platform Payment account to AccountTypeToCheck +/// Error returned when trying to convert a Platform-only account (Platform +/// Payment or DIP-13 identity authentication) to [`AccountTypeToCheck`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct PlatformAccountConversionError; impl core::fmt::Display for PlatformAccountConversionError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "PlatformPayment accounts cannot be converted to AccountTypeToCheck") + write!(f, "Platform-only accounts cannot be converted to AccountTypeToCheck") } } @@ -231,6 +235,16 @@ impl TryFrom for AccountTypeToCheck { ManagedAccountType::IdentityInvitation { .. } => Ok(AccountTypeToCheck::IdentityInvitation), + ManagedAccountType::IdentityAuthenticationEcdsa { + .. + } + | ManagedAccountType::IdentityAuthenticationBls { + .. + } => { + // DIP-13 per-identity authentication accounts are Platform-only, + // operating on Dash Platform rather than the Core chain. + Err(PlatformAccountConversionError) + } ManagedAccountType::AssetLockAddressTopUp { .. } => Ok(AccountTypeToCheck::AssetLockAddressTopUp), @@ -296,6 +310,16 @@ impl TryFrom<&ManagedAccountType> for AccountTypeToCheck { ManagedAccountType::IdentityInvitation { .. } => Ok(AccountTypeToCheck::IdentityInvitation), + ManagedAccountType::IdentityAuthenticationEcdsa { + .. + } + | ManagedAccountType::IdentityAuthenticationBls { + .. + } => { + // DIP-13 per-identity authentication accounts are Platform-only, + // operating on Dash Platform rather than the Core chain. + Err(PlatformAccountConversionError) + } ManagedAccountType::AssetLockAddressTopUp { .. } => Ok(AccountTypeToCheck::AssetLockAddressTopUp), diff --git a/key-wallet/src/wallet/accounts.rs b/key-wallet/src/wallet/accounts.rs index 700a57ff3..59aa6d095 100644 --- a/key-wallet/src/wallet/accounts.rs +++ b/key-wallet/src/wallet/accounts.rs @@ -122,10 +122,13 @@ impl Wallet { /// Add a new BLS account to the wallet /// - /// BLS accounts are used for Platform/masternode operations. + /// BLS accounts are used for Platform/masternode operations. Accepts + /// [`AccountType::ProviderOperatorKeys`] and + /// [`AccountType::IdentityAuthenticationBls`]. /// /// # Arguments - /// * `account_type` - The type of account (must be ProviderOperatorKeys) + /// * `account_type` - The type of account (must be ProviderOperatorKeys + /// or IdentityAuthenticationBls) /// * `bls_seed` - Optional 32-byte seed for BLS key generation. If not provided, /// the account will be derived from the wallet's private key. /// @@ -138,9 +141,13 @@ impl Wallet { bls_seed: Option<[u8; 32]>, ) -> Result<()> { // Validate account type - if !matches!(account_type, AccountType::ProviderOperatorKeys) { + if !matches!( + account_type, + AccountType::ProviderOperatorKeys | AccountType::IdentityAuthenticationBls { .. } + ) { return Err(Error::InvalidParameter( - "BLS accounts can only be ProviderOperatorKeys".to_string(), + "BLS accounts can only be ProviderOperatorKeys or IdentityAuthenticationBls" + .to_string(), )); } @@ -197,9 +204,13 @@ impl Wallet { passphrase: &str, ) -> Result<()> { // Validate account type - if !matches!(account_type, AccountType::ProviderOperatorKeys) { + if !matches!( + account_type, + AccountType::ProviderOperatorKeys | AccountType::IdentityAuthenticationBls { .. } + ) { return Err(Error::InvalidParameter( - "BLS accounts can only be ProviderOperatorKeys".to_string(), + "BLS accounts can only be ProviderOperatorKeys or IdentityAuthenticationBls" + .to_string(), )); } From 7a0aab357335b2e570b3f3dc02550db80defddd4 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 23 Apr 2026 14:07:50 +0800 Subject: [PATCH 2/5] fix(key-wallet): expose BLS derivation for IdentityAuthenticationBls `next_bls_operator_key` only matched `ProviderOperatorKeys`, so the DIP-13 identity-authentication BLS pool had no API to derive fresh keys (the ECDSA `account_xpub` taken by `next_address` is unusable for BLS). Extract the shared pool-derivation logic and add `next_bls_identity_authentication_key` so `IdentityAuthenticationBls` can be consumed through the same path. Co-Authored-By: Claude Opus 4.7 (1M context) --- key-wallet/src/managed_account/mod.rs | 110 ++++++++++++++++---------- 1 file changed, 68 insertions(+), 42 deletions(-) diff --git a/key-wallet/src/managed_account/mod.rs b/key-wallet/src/managed_account/mod.rs index 6eef7e29b..4bd343f6e 100644 --- a/key-wallet/src/managed_account/mod.rs +++ b/key-wallet/src/managed_account/mod.rs @@ -858,12 +858,12 @@ impl ManagedCoreAccount { addresses, .. } => { - // `account_xpub` is an ECDSA extended pubkey and is useless for a - // BLS key pool. Callers that need to generate BLS identity-auth - // addresses must go through a dedicated BLS-aware API path - // (similar to `next_bls_operator_key` for ProviderOperatorKeys). - // Here we only allow progression when the pool already has a - // pre-derived address cached (NoKeySource). + // `account_xpub` is an ECDSA extended pubkey and is useless for + // a BLS key pool. Callers that need to derive fresh BLS + // identity-auth keys must go through + // `next_bls_identity_authentication_key`, which takes a BLS + // xpub. Here we only allow progression when the pool already + // has a pre-derived address cached (NoKeySource). addresses.next_unused(&address_pool::KeySource::NoKeySource, add_to_state).map_err( |e| match e { crate::error::Error::NoKeySource => { @@ -978,12 +978,12 @@ impl ManagedCoreAccount { addresses, .. } => { - // `account_xpub` is an ECDSA extended pubkey and is useless for a - // BLS key pool. Callers that need to generate BLS identity-auth - // addresses must go through a dedicated BLS-aware API path - // (similar to `next_bls_operator_key` for ProviderOperatorKeys). - // Here we only allow progression when the pool already has a - // pre-derived address cached (NoKeySource). + // `account_xpub` is an ECDSA extended pubkey and is useless for + // a BLS key pool. Callers that need to derive fresh BLS + // identity-auth keys must go through + // `next_bls_identity_authentication_key`, which takes a BLS + // xpub. Here we only allow progression when the pool already + // has a pre-derived address cached (NoKeySource). addresses .next_unused_with_info( &address_pool::KeySource::NoKeySource, @@ -1016,6 +1016,39 @@ impl ManagedCoreAccount { } } + /// Derive the next BLS public key from an address pool. + /// + /// Shared by [`Self::next_bls_operator_key`] and + /// [`Self::next_bls_identity_authentication_key`]. + #[cfg(feature = "bls")] + fn next_bls_key_from_pool( + addresses: &mut address_pool::AddressPool, + account_xpub: Option, + add_to_state: bool, + ) -> Result, &'static str> { + let key_source = match account_xpub { + Some(xpub) => address_pool::KeySource::BLSPublic(xpub), + None => address_pool::KeySource::NoKeySource, + }; + + let info = addresses + .next_unused_with_info(&key_source, add_to_state) + .map_err(|_| "Failed to get next unused address")?; + + let Some(PublicKeyType::BLS(pub_key_bytes)) = info.public_key else { + return Err("Expected BLS public key but got different key type"); + }; + + addresses.mark_index_used(info.index); + + use dashcore::blsful::{Bls12381G2Impl, PublicKey, SerializationFormat}; + PublicKey::::from_bytes_with_mode( + &pub_key_bytes, + SerializationFormat::Modern, + ) + .map_err(|_| "Failed to deserialize BLS public key") + } + /// Generate the next BLS operator key (only for ProviderOperatorKeys accounts) /// Returns the BLS public key at the next unused index #[cfg(feature = "bls")] @@ -1028,40 +1061,33 @@ impl ManagedCoreAccount { ManagedAccountType::ProviderOperatorKeys { addresses, .. - } => { - // Create key source from the optional BLS public key - let key_source = match account_xpub { - Some(xpub) => address_pool::KeySource::BLSPublic(xpub), - None => address_pool::KeySource::NoKeySource, - }; - - // Use next_unused_with_info to get the next address (handles caching and derivation) - let info = addresses - .next_unused_with_info(&key_source, add_to_state) - .map_err(|_| "Failed to get next unused address")?; - - // Extract the BLS public key from the address info - let Some(PublicKeyType::BLS(pub_key_bytes)) = info.public_key else { - return Err("Expected BLS public key but got different key type"); - }; - - // Mark as used - addresses.mark_index_used(info.index); - - // Convert bytes to BLS public key - use dashcore::blsful::{Bls12381G2Impl, PublicKey, SerializationFormat}; - let public_key = PublicKey::::from_bytes_with_mode( - &pub_key_bytes, - SerializationFormat::Modern, - ) - .map_err(|_| "Failed to deserialize BLS public key")?; - - Ok(public_key) - } + } => Self::next_bls_key_from_pool(addresses, account_xpub, add_to_state), _ => Err("This method only works for ProviderOperatorKeys accounts"), } } + /// Generate the next BLS identity-authentication key + /// (only for `IdentityAuthenticationBls` accounts). + /// + /// Counterpart to [`Self::next_bls_operator_key`] for the DIP-13 + /// identity authentication BLS pool — the ECDSA `account_xpub` accepted + /// by [`Self::next_address`] is useless for BLS derivation, so this + /// method takes the BLS xpub directly. + #[cfg(feature = "bls")] + pub fn next_bls_identity_authentication_key( + &mut self, + account_xpub: Option, + add_to_state: bool, + ) -> Result, &'static str> { + match &mut self.account_type { + ManagedAccountType::IdentityAuthenticationBls { + addresses, + .. + } => Self::next_bls_key_from_pool(addresses, account_xpub, add_to_state), + _ => Err("This method only works for IdentityAuthenticationBls accounts"), + } + } + /// Generate the next EdDSA platform key (only for ProviderPlatformKeys accounts) /// Returns the Ed25519 public key and address info at the next unused index #[cfg(feature = "eddsa")] From 07f7e5ae521df0471ab57a37739071ca9fed2c24 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 23 Apr 2026 14:33:27 +0800 Subject: [PATCH 3/5] fix(key-wallet-ffi): migrate off removed FFIError APIs #670 removed `FFIError::error`, `FFIError::set_error`, `FFIError::success`, and `FFIError::free_message` in favour of `FFIError::set`, `FFIError::clean`, and a `Drop` impl. The DIP-13 identity-authentication account additions in this PR still called the removed helpers, breaking `key-wallet-ffi` after merging the latest v0.42-dev. Replace each call site with the new API: - `FFIError::set_error(err, code, msg)` -> `(*err).set(code, &msg)` - `FFIError::error(code, msg)` -> struct literal; caller-owned, freed by Drop - `e.free_message()` -> let `FFIError`'s Drop clean up Co-Authored-By: Claude Opus 4.7 (1M context) --- key-wallet-ffi/src/account.rs | 7 +-- key-wallet-ffi/src/address_pool.rs | 27 ++++----- key-wallet-ffi/src/managed_account.rs | 7 +-- key-wallet-ffi/src/types.rs | 83 +++++++++++++-------------- key-wallet-ffi/src/wallet.rs | 21 +++---- 5 files changed, 68 insertions(+), 77 deletions(-) diff --git a/key-wallet-ffi/src/account.rs b/key-wallet-ffi/src/account.rs index 6b393593f..3fbe33ef1 100644 --- a/key-wallet-ffi/src/account.rs +++ b/key-wallet-ffi/src/account.rs @@ -92,15 +92,14 @@ pub unsafe extern "C" fn wallet_get_account( let wallet = &*wallet; let account_type_rust = match account_type.to_account_type(account_index) { Ok(t) => t, - Err(mut err) => { + Err(err) => { let code = err.code; let message = if err.message.is_null() { "Invalid account type".to_string() } else { - let msg = std::ffi::CStr::from_ptr(err.message).to_string_lossy().to_string(); - err.free_message(); - msg + std::ffi::CStr::from_ptr(err.message).to_string_lossy().into_owned() }; + // `err` dropped here; its original message is freed by `Drop`. return FFIAccountResult::error(code, message); } }; diff --git a/key-wallet-ffi/src/address_pool.rs b/key-wallet-ffi/src/address_pool.rs index a6310d8e3..4656a3381 100644 --- a/key-wallet-ffi/src/address_pool.rs +++ b/key-wallet-ffi/src/address_pool.rs @@ -309,15 +309,14 @@ pub unsafe extern "C" fn managed_wallet_get_address_pool_info( let account_type_rust = match account_type.to_account_type(account_index) { Ok(t) => t, - Err(mut e) => { + Err(e) => { let msg = if e.message.is_null() { "Invalid account type".to_string() } else { - let m = std::ffi::CStr::from_ptr(e.message).to_string_lossy().to_string(); - e.free_message(); - m + std::ffi::CStr::from_ptr(e.message).to_string_lossy().into_owned() }; - FFIError::set_error(error, e.code, msg); + (*error).set(e.code, &msg); + // `e` dropped here; its original message is freed by `Drop`. return false; } }; @@ -411,15 +410,14 @@ pub unsafe extern "C" fn managed_wallet_set_gap_limit( let account_type_rust = match account_type.to_account_type(account_index) { Ok(t) => t, - Err(mut e) => { + Err(e) => { let msg = if e.message.is_null() { "Invalid account type".to_string() } else { - let m = std::ffi::CStr::from_ptr(e.message).to_string_lossy().to_string(); - e.free_message(); - m + std::ffi::CStr::from_ptr(e.message).to_string_lossy().into_owned() }; - FFIError::set_error(error, e.code, msg); + (*error).set(e.code, &msg); + // `e` dropped here; its original message is freed by `Drop`. return false; } }; @@ -504,15 +502,14 @@ pub unsafe extern "C" fn managed_wallet_generate_addresses_to_index( let account_type_rust = match account_type.to_account_type(account_index) { Ok(t) => t, - Err(mut e) => { + Err(e) => { let msg = if e.message.is_null() { "Invalid account type".to_string() } else { - let m = std::ffi::CStr::from_ptr(e.message).to_string_lossy().to_string(); - e.free_message(); - m + std::ffi::CStr::from_ptr(e.message).to_string_lossy().into_owned() }; - FFIError::set_error(error, e.code, msg); + (*error).set(e.code, &msg); + // `e` dropped here; its original message is freed by `Drop`. return false; } }; diff --git a/key-wallet-ffi/src/managed_account.rs b/key-wallet-ffi/src/managed_account.rs index d72af917e..00f4561eb 100644 --- a/key-wallet-ffi/src/managed_account.rs +++ b/key-wallet-ffi/src/managed_account.rs @@ -223,15 +223,14 @@ pub unsafe extern "C" fn managed_wallet_get_account( let managed_wallet = &*managed_wallet_ptr; let account_type_rust = match account_type.to_account_type(account_index) { Ok(t) => t, - Err(mut e) => { + Err(e) => { let code = e.code; let message = if e.message.is_null() { "Invalid account type".to_string() } else { - let m = std::ffi::CStr::from_ptr(e.message).to_string_lossy().to_string(); - e.free_message(); - m + std::ffi::CStr::from_ptr(e.message).to_string_lossy().into_owned() }; + // `e` dropped here; its original message is freed by `Drop`. // `wallet_manager_get_managed_wallet_info` allocated `managed_wallet_ptr` // above; the success path frees it via `managed_wallet_info_free` at the // bottom of this function. Do the same here before bailing so the diff --git a/key-wallet-ffi/src/types.rs b/key-wallet-ffi/src/types.rs index cd619b940..fdadfea9a 100644 --- a/key-wallet-ffi/src/types.rs +++ b/key-wallet-ffi/src/types.rs @@ -8,6 +8,7 @@ use key_wallet::managed_account::transaction_record::{OutputRole, TransactionDir use key_wallet::transaction_checking::transaction_router::TransactionType; use key_wallet::transaction_checking::{BlockInfo, TransactionContext}; use key_wallet::Wallet; +use std::ffi::CString; use std::os::raw::c_char; use std::sync::Arc; @@ -302,31 +303,40 @@ impl FFIAccountType { // conversion. Callers must use the dedicated FFI entry points (e.g. // `wallet_add_dashpay_receiving_account`, `wallet_add_platform_payment_account`). FFIAccountType::DashpayReceivingFunds => { - return Err(FFIError::error( - FFIErrorCode::InvalidInput, - "FFIAccountType::DashpayReceivingFunds cannot be converted to AccountType \ - without user_identity_id and friend_identity_id. Use \ - wallet_add_dashpay_receiving_account() instead." - .to_string(), - )); + return Err(FFIError { + code: FFIErrorCode::InvalidInput, + message: CString::new( + "FFIAccountType::DashpayReceivingFunds cannot be converted to \ + AccountType without user_identity_id and friend_identity_id. Use \ + wallet_add_dashpay_receiving_account() instead.", + ) + .unwrap_or_default() + .into_raw(), + }); } FFIAccountType::DashpayExternalAccount => { - return Err(FFIError::error( - FFIErrorCode::InvalidInput, - "FFIAccountType::DashpayExternalAccount cannot be converted to AccountType \ - without user_identity_id and friend_identity_id. Use \ - wallet_add_dashpay_external_account_with_xpub_bytes() instead." - .to_string(), - )); + return Err(FFIError { + code: FFIErrorCode::InvalidInput, + message: CString::new( + "FFIAccountType::DashpayExternalAccount cannot be converted to \ + AccountType without user_identity_id and friend_identity_id. Use \ + wallet_add_dashpay_external_account_with_xpub_bytes() instead.", + ) + .unwrap_or_default() + .into_raw(), + }); } FFIAccountType::PlatformPayment => { - return Err(FFIError::error( - FFIErrorCode::InvalidInput, - "FFIAccountType::PlatformPayment cannot be converted to AccountType \ - without account and key_class indices. Use \ - wallet_add_platform_payment_account() instead." - .to_string(), - )); + return Err(FFIError { + code: FFIErrorCode::InvalidInput, + message: CString::new( + "FFIAccountType::PlatformPayment cannot be converted to AccountType \ + without account and key_class indices. Use \ + wallet_add_platform_payment_account() instead.", + ) + .unwrap_or_default() + .into_raw(), + }); } }) } @@ -683,15 +693,13 @@ impl FFIWalletAccountCreationOptions { ); let mut accounts = Vec::new(); for &ffi_type in slice { - match ffi_type.to_account_type(0) { - Ok(account_type) => accounts.push(account_type), - Err(mut err) => { - // Free the error message allocated by - // FFIError::error to avoid a leak since we - // cannot propagate the error from this - // signature. - err.free_message(); - } + // Errors are silently dropped — callers must use the + // dedicated entry points for account types that need + // more than a single u32 index, and this signature has + // no error-return channel. `FFIError::Drop` frees the + // allocated message. + if let Ok(account_type) = ffi_type.to_account_type(0) { + accounts.push(account_type); } } Some(accounts) @@ -998,10 +1006,7 @@ mod tests { let result = FFIAccountType::DashpayReceivingFunds.to_account_type(0); let err = result.expect_err("should be an error"); assert_eq!(err.code, FFIErrorCode::InvalidInput); - unsafe { - let mut err = err; - err.free_message(); - } + // FFIError's Drop impl frees the message. } #[test] @@ -1010,10 +1015,7 @@ mod tests { let result = FFIAccountType::DashpayExternalAccount.to_account_type(0); let err = result.expect_err("should be an error"); assert_eq!(err.code, FFIErrorCode::InvalidInput); - unsafe { - let mut err = err; - err.free_message(); - } + // FFIError's Drop impl frees the message. } #[test] @@ -1022,10 +1024,7 @@ mod tests { let result = FFIAccountType::PlatformPayment.to_account_type(0); let err = result.expect_err("should be an error"); assert_eq!(err.code, FFIErrorCode::InvalidInput); - unsafe { - let mut err = err; - err.free_message(); - } + // FFIError's Drop impl frees the message. } #[test] diff --git a/key-wallet-ffi/src/wallet.rs b/key-wallet-ffi/src/wallet.rs index 97c19c6fe..3d914c0aa 100644 --- a/key-wallet-ffi/src/wallet.rs +++ b/key-wallet-ffi/src/wallet.rs @@ -381,15 +381,14 @@ pub unsafe extern "C" fn wallet_add_account( let account_type_rust = match account_type.to_account_type(account_index) { Ok(t) => t, - Err(mut e) => { + Err(e) => { let code = e.code; let message = if e.message.is_null() { "Invalid account type".to_string() } else { - let m = std::ffi::CStr::from_ptr(e.message).to_string_lossy().to_string(); - e.free_message(); - m + std::ffi::CStr::from_ptr(e.message).to_string_lossy().into_owned() }; + // `e` dropped here; its original message is freed by `Drop`. return crate::types::FFIAccountResult::error(code, message); } }; @@ -623,15 +622,14 @@ pub unsafe extern "C" fn wallet_add_account_with_xpub_bytes( let account_type_rust = match account_type.to_account_type(account_index) { Ok(t) => t, - Err(mut e) => { + Err(e) => { let code = e.code; let message = if e.message.is_null() { "Invalid account type".to_string() } else { - let m = std::ffi::CStr::from_ptr(e.message).to_string_lossy().to_string(); - e.free_message(); - m + std::ffi::CStr::from_ptr(e.message).to_string_lossy().into_owned() }; + // `e` dropped here; its original message is freed by `Drop`. return crate::types::FFIAccountResult::error(code, message); } }; @@ -759,15 +757,14 @@ pub unsafe extern "C" fn wallet_add_account_with_string_xpub( let account_type_rust = match account_type.to_account_type(account_index) { Ok(t) => t, - Err(mut e) => { + Err(e) => { let code = e.code; let message = if e.message.is_null() { "Invalid account type".to_string() } else { - let m = std::ffi::CStr::from_ptr(e.message).to_string_lossy().to_string(); - e.free_message(); - m + std::ffi::CStr::from_ptr(e.message).to_string_lossy().into_owned() }; + // `e` dropped here; its original message is freed by `Drop`. return crate::types::FFIAccountResult::error(code, message); } }; From df09f7daf574337bcf07a51e92cf7f470fd17c32 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 23 Apr 2026 17:04:10 +0800 Subject: [PATCH 4/5] refactor(key-wallet): drop managed-side DIP-13 identity authentication plumbing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ManagedCoreAccount` is designed around L1 state (balance/transactions/ utxos/spent_outpoints/monitor_revision), none of which applies to DIP-13 identity authentication keys — they are Platform-only signing keys that never hold L1 UTXOs and are already excluded from every `TransactionRouter` relevance set. The immutable `Account` in `AccountCollection` already carries the xpub/derivation path needed to derive signing keys, so the managed-side wrapper is dead weight. Remove: - `ManagedAccountCollection::identity_authentication_{ecdsa,bls}` fields + all wiring (`new`/`insert`/`contains`/`all_accounts*`/`is_empty`/ `clear`/`from_account_collection`) - `ManagedAccountType::IdentityAuthentication{Ecdsa,Bls}` variants and their match arms across `managed_account/mod.rs`, `transaction_checking/*`, and `key-wallet-ffi` - `next_bls_identity_authentication_key()` (became dead code) Preserve `AccountType::IdentityAuthentication{Ecdsa,Bls}` and the immutable-side `AccountCollection` fields, DIP-9 constants, and the FFI `FFIAccountType = 16/17` mappings — those stay on the immutable path. `from_account_type` / `create_managed_account_from_account_type` still receive the immutable variants in their signatures, so those arms now return `Err(InvalidParameter)`; the path is unreachable in practice because `from_account_collection` no longer feeds them. Co-Authored-By: Claude Opus 4.7 (1M context) --- key-wallet-ffi/src/address_pool.rs | 44 +++---- key-wallet-ffi/src/managed_account.rs | 23 ++-- .../managed_account_collection.rs | 99 +++------------- .../managed_account/managed_account_type.rs | 108 ++---------------- key-wallet/src/managed_account/mod.rs | 90 +-------------- .../transaction_checking/account_checker.rs | 11 -- .../transaction_router/mod.rs | 20 ---- 7 files changed, 53 insertions(+), 342 deletions(-) diff --git a/key-wallet-ffi/src/address_pool.rs b/key-wallet-ffi/src/address_pool.rs index 4656a3381..3b4224ae5 100644 --- a/key-wallet-ffi/src/address_pool.rs +++ b/key-wallet-ffi/src/address_pool.rs @@ -47,11 +47,15 @@ fn get_managed_account_by_type<'a>( } AccountType::IdentityInvitation => collection.identity_invitation.as_ref(), AccountType::IdentityAuthenticationEcdsa { - identity_index, - } => collection.identity_authentication_ecdsa.get(identity_index), - AccountType::IdentityAuthenticationBls { - identity_index, - } => collection.identity_authentication_bls.get(identity_index), + .. + } + | AccountType::IdentityAuthenticationBls { + .. + } => { + // DIP-13 per-identity authentication accounts are Platform-only + // and have no managed-side representation. + None + } AccountType::AssetLockAddressTopUp => collection.asset_lock_address_topup.as_ref(), AccountType::AssetLockShieldedAddressTopUp => { collection.asset_lock_shielded_address_topup.as_ref() @@ -106,11 +110,15 @@ fn get_managed_account_by_type_mut<'a>( } AccountType::IdentityInvitation => collection.identity_invitation.as_mut(), AccountType::IdentityAuthenticationEcdsa { - identity_index, - } => collection.identity_authentication_ecdsa.get_mut(identity_index), - AccountType::IdentityAuthenticationBls { - identity_index, - } => collection.identity_authentication_bls.get_mut(identity_index), + .. + } + | AccountType::IdentityAuthenticationBls { + .. + } => { + // DIP-13 per-identity authentication accounts are Platform-only + // and have no managed-side representation. + None + } AccountType::AssetLockAddressTopUp => collection.asset_lock_address_topup.as_mut(), AccountType::AssetLockShieldedAddressTopUp => { collection.asset_lock_shielded_address_topup.as_mut() @@ -718,22 +726,6 @@ pub unsafe extern "C" fn managed_wallet_mark_address_used( } } } - if !found { - for account in collection.identity_authentication_ecdsa.values_mut() { - if account.mark_address_used(&address) { - found = true; - break; - } - } - } - if !found { - for account in collection.identity_authentication_bls.values_mut() { - if account.mark_address_used(&address) { - found = true; - break; - } - } - } if !found { if let Some(account) = &mut collection.asset_lock_address_topup { if account.mark_address_used(&address) { diff --git a/key-wallet-ffi/src/managed_account.rs b/key-wallet-ffi/src/managed_account.rs index 00f4561eb..56cec0432 100644 --- a/key-wallet-ffi/src/managed_account.rs +++ b/key-wallet-ffi/src/managed_account.rs @@ -268,11 +268,16 @@ pub unsafe extern "C" fn managed_wallet_get_account( } AccountType::IdentityInvitation => managed_collection.identity_invitation.as_ref(), AccountType::IdentityAuthenticationEcdsa { - identity_index, - } => managed_collection.identity_authentication_ecdsa.get(&identity_index), - AccountType::IdentityAuthenticationBls { - identity_index, - } => managed_collection.identity_authentication_bls.get(&identity_index), + .. + } + | AccountType::IdentityAuthenticationBls { + .. + } => { + // DIP-13 per-identity authentication accounts are Platform-only + // and have no managed-side representation. Signing keys are + // derived from the immutable `Account` directly. + None + } AccountType::AssetLockAddressTopUp => { managed_collection.asset_lock_address_topup.as_ref() } @@ -1165,14 +1170,6 @@ pub unsafe extern "C" fn managed_core_account_get_address_pool( addresses, .. } => addresses, - ManagedAccountType::IdentityAuthenticationEcdsa { - addresses, - .. - } => addresses, - ManagedAccountType::IdentityAuthenticationBls { - addresses, - .. - } => addresses, }; let ffi_pool = FFIAddressPool { diff --git a/key-wallet/src/managed_account/managed_account_collection.rs b/key-wallet/src/managed_account/managed_account_collection.rs index 045097661..b38d021ec 100644 --- a/key-wallet/src/managed_account/managed_account_collection.rs +++ b/key-wallet/src/managed_account/managed_account_collection.rs @@ -126,16 +126,6 @@ pub struct ManagedAccountCollection { pub identity_topup_not_bound: Option, /// Identity invitation account (optional) pub identity_invitation: Option, - /// Per-identity ECDSA authentication accounts (DIP-13), keyed by - /// `identity_index`. Platform-only — carries no L1 UTXOs. - pub identity_authentication_ecdsa: BTreeMap, - /// Per-identity BLS authentication accounts (DIP-13), keyed by - /// `identity_index`. Platform-only. - /// - /// Storage type is `ManagedCoreAccount`, which is not BLS-dependent, so the - /// field is available regardless of the `bls` feature — mirroring how - /// `provider_operator_keys` is handled at the managed-account level. - pub identity_authentication_bls: BTreeMap, /// Asset lock address top-up account (optional) pub asset_lock_address_topup: Option, /// Asset lock shielded address top-up account (optional) @@ -168,8 +158,6 @@ impl ManagedAccountCollection { identity_topup: BTreeMap::new(), identity_topup_not_bound: None, identity_invitation: None, - identity_authentication_ecdsa: BTreeMap::new(), - identity_authentication_bls: BTreeMap::new(), asset_lock_address_topup: None, asset_lock_shielded_address_topup: None, provider_voting_keys: None, @@ -216,14 +204,6 @@ impl ManagedAccountCollection { ManagedAccountType::IdentityInvitation { .. } => self.identity_invitation.is_some(), - ManagedAccountType::IdentityAuthenticationEcdsa { - identity_index, - .. - } => self.identity_authentication_ecdsa.contains_key(identity_index), - ManagedAccountType::IdentityAuthenticationBls { - identity_index, - .. - } => self.identity_authentication_bls.contains_key(identity_index), ManagedAccountType::AssetLockAddressTopUp { .. } => self.asset_lock_address_topup.is_some(), @@ -329,18 +309,6 @@ impl ManagedAccountCollection { } => { self.identity_invitation = Some(account); } - ManagedAccountType::IdentityAuthenticationEcdsa { - identity_index, - .. - } => { - self.identity_authentication_ecdsa.insert(*identity_index, account); - } - ManagedAccountType::IdentityAuthenticationBls { - identity_index, - .. - } => { - self.identity_authentication_bls.insert(*identity_index, account); - } ManagedAccountType::AssetLockAddressTopUp { .. } => { @@ -470,20 +438,10 @@ impl ManagedAccountCollection { } } - // Convert per-identity ECDSA authentication accounts - for (index, account) in &account_collection.identity_authentication_ecdsa { - if let Ok(managed_account) = Self::create_managed_account_from_account(account) { - managed_collection.identity_authentication_ecdsa.insert(*index, managed_account); - } - } - - // Convert per-identity BLS authentication accounts - #[cfg(feature = "bls")] - for (index, account) in &account_collection.identity_authentication_bls { - if let Ok(managed_account) = Self::create_managed_account_from_bls_account(account) { - managed_collection.identity_authentication_bls.insert(*index, managed_account); - } - } + // DIP-13 per-identity authentication accounts are Platform-only and + // have no managed-side representation. Signing keys are derived on + // demand from the immutable `Account`'s xpub / derivation path, which + // lives on `account_collection` directly. if let Some(account) = &account_collection.asset_lock_address_topup { if let Ok(managed_account) = Self::create_managed_account_from_account(account) { @@ -713,36 +671,17 @@ impl ManagedAccountCollection { } } AccountType::IdentityAuthenticationEcdsa { - identity_index, - } => { - // DIP-13: hardened leaves per the spec. - let addresses = AddressPool::new( - base_path, - AddressPoolType::AbsentHardened, - DEFAULT_SPECIAL_GAP_LIMIT, - network, - key_source, - )?; - ManagedAccountType::IdentityAuthenticationEcdsa { - identity_index, - addresses, - } + .. } - AccountType::IdentityAuthenticationBls { - identity_index, + | AccountType::IdentityAuthenticationBls { + .. } => { - // DIP-13: hardened leaves per the spec. - let addresses = AddressPool::new( - base_path, - AddressPoolType::AbsentHardened, - DEFAULT_SPECIAL_GAP_LIMIT, - network, - key_source, - )?; - ManagedAccountType::IdentityAuthenticationBls { - identity_index, - addresses, - } + // DIP-13 per-identity authentication accounts are Platform-only + // and have no managed-side representation. + return Err(crate::error::Error::InvalidParameter( + "DIP-13 identity authentication accounts have no managed representation" + .to_string(), + )); } AccountType::AssetLockAddressTopUp => { let addresses = AddressPool::new( @@ -1041,10 +980,6 @@ impl ManagedAccountCollection { accounts.push(account); } - accounts.extend(self.identity_authentication_ecdsa.values()); - - accounts.extend(self.identity_authentication_bls.values()); - if let Some(account) = &self.asset_lock_address_topup { accounts.push(account); } @@ -1104,10 +1039,6 @@ impl ManagedAccountCollection { accounts.push(account); } - accounts.extend(self.identity_authentication_ecdsa.values_mut()); - - accounts.extend(self.identity_authentication_bls.values_mut()); - if let Some(account) = &mut self.asset_lock_address_topup { accounts.push(account); } @@ -1172,8 +1103,6 @@ impl ManagedAccountCollection { && self.identity_topup.is_empty() && self.identity_topup_not_bound.is_none() && self.identity_invitation.is_none() - && self.identity_authentication_ecdsa.is_empty() - && self.identity_authentication_bls.is_empty() && self.asset_lock_address_topup.is_none() && self.asset_lock_shielded_address_topup.is_none() && self.provider_voting_keys.is_none() @@ -1194,8 +1123,6 @@ impl ManagedAccountCollection { self.identity_topup.clear(); self.identity_topup_not_bound = None; self.identity_invitation = None; - self.identity_authentication_ecdsa.clear(); - self.identity_authentication_bls.clear(); self.asset_lock_address_topup = None; self.asset_lock_shielded_address_topup = None; self.provider_voting_keys = None; diff --git a/key-wallet/src/managed_account/managed_account_type.rs b/key-wallet/src/managed_account/managed_account_type.rs index bb492585d..8dfe44984 100644 --- a/key-wallet/src/managed_account/managed_account_type.rs +++ b/key-wallet/src/managed_account/managed_account_type.rs @@ -58,27 +58,6 @@ pub enum ManagedAccountType { /// Identity invitation address pool addresses: AddressPool, }, - /// Per-identity ECDSA authentication keys (DIP-13). - /// Path: `m/9'/coin_type'/5'/0'/0'/identity_index'`. Platform-only. - IdentityAuthenticationEcdsa { - /// Which identity in this wallet these keys belong to (hardened). - identity_index: u32, - /// Address pool for sequential key_index'. - addresses: AddressPool, - }, - /// Per-identity BLS authentication keys (DIP-13). - /// Path: `m/9'/coin_type'/5'/0'/1'/identity_index'`. Platform-only. - /// - /// The variant is always present regardless of the `bls` feature — only - /// `BLSAccount`-typed storage is cfg-gated. This keeps downstream pattern - /// matches on `ManagedAccountType` exhaustive in both feature - /// configurations. - IdentityAuthenticationBls { - /// Which identity in this wallet these keys belong to (hardened). - identity_index: u32, - /// Address pool for sequential key_index'. - addresses: AddressPool, - }, /// Asset lock address top-up funding (subfeature 4) /// Path: m/9'/coinType'/5'/4'/index' AssetLockAddressTopUp { @@ -176,9 +155,6 @@ impl ManagedAccountType { | Self::IdentityInvitation { .. } - | Self::IdentityAuthenticationEcdsa { - .. - } | Self::AssetLockAddressTopUp { .. } @@ -209,9 +185,6 @@ impl ManagedAccountType { account, .. } => Some(*account), - Self::IdentityAuthenticationBls { - .. - } => None, } } @@ -261,10 +234,6 @@ impl ManagedAccountType { addresses, .. } - | Self::IdentityAuthenticationEcdsa { - addresses, - .. - } | Self::AssetLockAddressTopUp { addresses, .. @@ -302,10 +271,6 @@ impl ManagedAccountType { | Self::PlatformPayment { addresses, .. - } - | Self::IdentityAuthenticationBls { - addresses, - .. } => vec![addresses], } } @@ -340,10 +305,6 @@ impl ManagedAccountType { addresses, .. } - | Self::IdentityAuthenticationEcdsa { - addresses, - .. - } | Self::AssetLockAddressTopUp { addresses, .. @@ -381,10 +342,6 @@ impl ManagedAccountType { | Self::PlatformPayment { addresses, .. - } - | Self::IdentityAuthenticationBls { - addresses, - .. } => vec![addresses], } } @@ -469,18 +426,6 @@ impl ManagedAccountType { Self::IdentityInvitation { .. } => AccountType::IdentityInvitation, - Self::IdentityAuthenticationEcdsa { - identity_index, - .. - } => AccountType::IdentityAuthenticationEcdsa { - identity_index: *identity_index, - }, - Self::IdentityAuthenticationBls { - identity_index, - .. - } => AccountType::IdentityAuthenticationBls { - identity_index: *identity_index, - }, Self::AssetLockAddressTopUp { .. } => AccountType::AssetLockAddressTopUp, @@ -663,50 +608,19 @@ impl ManagedAccountType { }) } AccountType::IdentityAuthenticationEcdsa { - identity_index, - } => { - // Identity authentication keys are signing/authentication - // material; falling back to the master path on a derivation - // error would produce a subtly-wrong pool at `m/`, so we - // propagate the error instead. - let path = account_type.derivation_path(network)?; - // Hardened leaves per DIP-13 — addresses must be generated from - // a private-key-bearing source (watch-only/no-key setups will - // create an empty pool that can still track derivation paths). - let pool = AddressPool::new( - path, - AddressPoolType::AbsentHardened, - DEFAULT_SPECIAL_GAP_LIMIT, - network, - key_source, - )?; - - Ok(Self::IdentityAuthenticationEcdsa { - identity_index, - addresses: pool, - }) + .. } - AccountType::IdentityAuthenticationBls { - identity_index, + | AccountType::IdentityAuthenticationBls { + .. } => { - // Identity authentication keys are signing/authentication - // material; falling back to the master path on a derivation - // error would produce a subtly-wrong pool at `m/`, so we - // propagate the error instead. - let path = account_type.derivation_path(network)?; - // Hardened leaves per DIP-13. - let pool = AddressPool::new( - path, - AddressPoolType::AbsentHardened, - DEFAULT_SPECIAL_GAP_LIMIT, - network, - key_source, - )?; - - Ok(Self::IdentityAuthenticationBls { - identity_index, - addresses: pool, - }) + // DIP-13 per-identity authentication accounts are Platform-only + // — they hold no L1 UTXOs and have no managed-side + // representation. Signing keys are derived on demand from the + // immutable `Account`'s xpub / derivation path. + Err(crate::error::Error::InvalidParameter( + "DIP-13 identity authentication accounts have no managed representation" + .to_string(), + )) } AccountType::AssetLockAddressTopUp => { let path = account_type diff --git a/key-wallet/src/managed_account/mod.rs b/key-wallet/src/managed_account/mod.rs index 4bd343f6e..c58c296d0 100644 --- a/key-wallet/src/managed_account/mod.rs +++ b/key-wallet/src/managed_account/mod.rs @@ -254,10 +254,6 @@ impl ManagedCoreAccount { addresses, .. } - | ManagedAccountType::IdentityAuthenticationEcdsa { - addresses, - .. - } | ManagedAccountType::AssetLockAddressTopUp { addresses, .. @@ -293,10 +289,6 @@ impl ManagedCoreAccount { | ManagedAccountType::PlatformPayment { addresses, .. - } - | ManagedAccountType::IdentityAuthenticationBls { - addresses, - .. } => { addresses.unused_addresses().first().and_then(|addr| addresses.address_index(addr)) } @@ -836,10 +828,6 @@ impl ManagedCoreAccount { | ManagedAccountType::PlatformPayment { addresses, .. - } - | ManagedAccountType::IdentityAuthenticationEcdsa { - addresses, - .. } => { // Create appropriate key source based on whether xpub is provided let key_source = match account_xpub { @@ -854,25 +842,6 @@ impl ManagedCoreAccount { _ => "Failed to generate address", }) } - ManagedAccountType::IdentityAuthenticationBls { - addresses, - .. - } => { - // `account_xpub` is an ECDSA extended pubkey and is useless for - // a BLS key pool. Callers that need to derive fresh BLS - // identity-auth keys must go through - // `next_bls_identity_authentication_key`, which takes a BLS - // xpub. Here we only allow progression when the pool already - // has a pre-derived address cached (NoKeySource). - addresses.next_unused(&address_pool::KeySource::NoKeySource, add_to_state).map_err( - |e| match e { - crate::error::Error::NoKeySource => { - "No unused addresses available and no key source provided" - } - _ => "Failed to generate address", - }, - ) - } ManagedAccountType::IdentityTopUp { addresses, .. @@ -956,10 +925,6 @@ impl ManagedCoreAccount { | ManagedAccountType::PlatformPayment { addresses, .. - } - | ManagedAccountType::IdentityAuthenticationEcdsa { - addresses, - .. } => { // Create appropriate key source based on whether xpub is provided let key_source = match account_xpub { @@ -974,28 +939,6 @@ impl ManagedCoreAccount { _ => "Failed to generate address with info", }) } - ManagedAccountType::IdentityAuthenticationBls { - addresses, - .. - } => { - // `account_xpub` is an ECDSA extended pubkey and is useless for - // a BLS key pool. Callers that need to derive fresh BLS - // identity-auth keys must go through - // `next_bls_identity_authentication_key`, which takes a BLS - // xpub. Here we only allow progression when the pool already - // has a pre-derived address cached (NoKeySource). - addresses - .next_unused_with_info( - &address_pool::KeySource::NoKeySource, - add_to_state, - ) - .map_err(|e| match e { - crate::error::Error::NoKeySource => { - "No unused addresses available and no key source provided" - } - _ => "Failed to generate address with info", - }) - } ManagedAccountType::IdentityTopUp { addresses, .. @@ -1018,8 +961,7 @@ impl ManagedCoreAccount { /// Derive the next BLS public key from an address pool. /// - /// Shared by [`Self::next_bls_operator_key`] and - /// [`Self::next_bls_identity_authentication_key`]. + /// Used by [`Self::next_bls_operator_key`]. #[cfg(feature = "bls")] fn next_bls_key_from_pool( addresses: &mut address_pool::AddressPool, @@ -1066,28 +1008,6 @@ impl ManagedCoreAccount { } } - /// Generate the next BLS identity-authentication key - /// (only for `IdentityAuthenticationBls` accounts). - /// - /// Counterpart to [`Self::next_bls_operator_key`] for the DIP-13 - /// identity authentication BLS pool — the ECDSA `account_xpub` accepted - /// by [`Self::next_address`] is useless for BLS derivation, so this - /// method takes the BLS xpub directly. - #[cfg(feature = "bls")] - pub fn next_bls_identity_authentication_key( - &mut self, - account_xpub: Option, - add_to_state: bool, - ) -> Result, &'static str> { - match &mut self.account_type { - ManagedAccountType::IdentityAuthenticationBls { - addresses, - .. - } => Self::next_bls_key_from_pool(addresses, account_xpub, add_to_state), - _ => Err("This method only works for IdentityAuthenticationBls accounts"), - } - } - /// Generate the next EdDSA platform key (only for ProviderPlatformKeys accounts) /// Returns the Ed25519 public key and address info at the next unused index #[cfg(feature = "eddsa")] @@ -1335,14 +1255,6 @@ impl ManagedCoreAccount { | ManagedAccountType::PlatformPayment { addresses, .. - } - | ManagedAccountType::IdentityAuthenticationEcdsa { - addresses, - .. - } - | ManagedAccountType::IdentityAuthenticationBls { - addresses, - .. } => Some(addresses.gap_limit), } } diff --git a/key-wallet/src/transaction_checking/account_checker.rs b/key-wallet/src/transaction_checking/account_checker.rs index 37241a714..a9750f1aa 100644 --- a/key-wallet/src/transaction_checking/account_checker.rs +++ b/key-wallet/src/transaction_checking/account_checker.rs @@ -760,17 +760,6 @@ impl ManagedCoreAccount { // They should never be checked for Core chain transactions. return None; } - ManagedAccountType::IdentityAuthenticationEcdsa { - .. - } - | ManagedAccountType::IdentityAuthenticationBls { - .. - } => { - // DIP-13 identity authentication keys are Platform-only — - // they never hold L1 UTXOs and should not be reported as - // relevant for Core chain transactions. - return None; - } }; Some(AccountMatch { diff --git a/key-wallet/src/transaction_checking/transaction_router/mod.rs b/key-wallet/src/transaction_checking/transaction_router/mod.rs index e39df1678..1ef8e59e6 100644 --- a/key-wallet/src/transaction_checking/transaction_router/mod.rs +++ b/key-wallet/src/transaction_checking/transaction_router/mod.rs @@ -235,16 +235,6 @@ impl TryFrom for AccountTypeToCheck { ManagedAccountType::IdentityInvitation { .. } => Ok(AccountTypeToCheck::IdentityInvitation), - ManagedAccountType::IdentityAuthenticationEcdsa { - .. - } - | ManagedAccountType::IdentityAuthenticationBls { - .. - } => { - // DIP-13 per-identity authentication accounts are Platform-only, - // operating on Dash Platform rather than the Core chain. - Err(PlatformAccountConversionError) - } ManagedAccountType::AssetLockAddressTopUp { .. } => Ok(AccountTypeToCheck::AssetLockAddressTopUp), @@ -310,16 +300,6 @@ impl TryFrom<&ManagedAccountType> for AccountTypeToCheck { ManagedAccountType::IdentityInvitation { .. } => Ok(AccountTypeToCheck::IdentityInvitation), - ManagedAccountType::IdentityAuthenticationEcdsa { - .. - } - | ManagedAccountType::IdentityAuthenticationBls { - .. - } => { - // DIP-13 per-identity authentication accounts are Platform-only, - // operating on Dash Platform rather than the Core chain. - Err(PlatformAccountConversionError) - } ManagedAccountType::AssetLockAddressTopUp { .. } => Ok(AccountTypeToCheck::AssetLockAddressTopUp), From 27c54d5dd515c2dae605bdda417c4ae634025e7b Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 23 Apr 2026 17:34:16 +0800 Subject: [PATCH 5/5] fix(key-wallet): resolve coderabbit findings on DIP-13 managed-side removal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three independent issues flagged on the previous commit: 1. `ManagedCoreAccount::from_account` / `from_bls_account` would panic on DIP-13 identity authentication `AccountType` variants. The primary path returns `Err(InvalidParameter)` (no managed representation) and the fallback to `NoKeySource` returns the same error — which the old `.expect("Should succeed with NoKeySource")` then turned into a panic. Make both constructors fallible, propagate the `InvalidParameter` early without hitting the fallback, and update the four call sites in `managed_accounts.rs` to use `?`. DIP-13 auth accounts are reachable via `AccountCollection::account_of_type` so this path is exercisable by callers. 2. `next_bls_key_from_pool` marked the address index used before attempting to deserialize the BLS public key, which consumed the index even when `PublicKey::from_bytes_with_mode` failed. Reorder so deserialization runs first and `mark_index_used` only runs on success. 3. `AccountTypeToCheck` rustdoc claimed both `ManagedAccountType`→`AccountTypeToCheck` and `AccountType`→`AccountTypeToCheck` return `PlatformAccountConversionError` for DIP-13 variants. After the managed-side removal only the immutable `AccountType` conversion does — `ManagedAccountType` rejects only `PlatformPayment`. Rewrite the doc to make that asymmetry explicit. Co-Authored-By: Claude Opus 4.7 (1M context) --- key-wallet/src/managed_account/mod.rs | 82 +++++++++++-------- .../transaction_router/mod.rs | 23 ++++-- .../managed_wallet_info/managed_accounts.rs | 8 +- 3 files changed, 69 insertions(+), 44 deletions(-) diff --git a/key-wallet/src/managed_account/mod.rs b/key-wallet/src/managed_account/mod.rs index c58c296d0..8ea308e7c 100644 --- a/key-wallet/src/managed_account/mod.rs +++ b/key-wallet/src/managed_account/mod.rs @@ -105,51 +105,63 @@ impl ManagedCoreAccount { self.spent_outpoints.contains(outpoint) } - /// Create a ManagedAccount from an Account - pub fn from_account(account: &super::Account) -> Self { + /// Create a ManagedAccount from an Account. + /// + /// Returns `Err` for account types that have no managed-side representation + /// (e.g. DIP-13 identity authentication accounts). Falls back to a + /// `NoKeySource` construction if the primary key-source path fails for any + /// other reason (e.g. hardened derivation from a public-only xpub). + pub fn from_account(account: &super::Account) -> Result { // Use the account's public key as the key source let key_source = address_pool::KeySource::Public(account.account_xpub); - let managed_type = ManagedAccountType::from_account_type( + let managed_type = match ManagedAccountType::from_account_type( account.account_type, account.network, &key_source, - ) - .unwrap_or_else(|_| { - // Fallback: create without pre-generated addresses - let no_key_source = address_pool::KeySource::NoKeySource; - ManagedAccountType::from_account_type( - account.account_type, - account.network, - &no_key_source, - ) - .expect("Should succeed with NoKeySource") - }); + ) { + Ok(managed) => managed, + Err(e @ crate::error::Error::InvalidParameter(_)) => return Err(e), + Err(_) => { + // Fallback: create without pre-generated addresses + let no_key_source = address_pool::KeySource::NoKeySource; + ManagedAccountType::from_account_type( + account.account_type, + account.network, + &no_key_source, + )? + } + }; - Self::new(managed_type, account.network, account.is_watch_only) + Ok(Self::new(managed_type, account.network, account.is_watch_only)) } - /// Create a ManagedAccount from a BLS Account + /// Create a ManagedAccount from a BLS Account. + /// + /// Returns `Err` for account types that have no managed-side representation + /// (e.g. DIP-13 identity authentication accounts). #[cfg(feature = "bls")] - pub fn from_bls_account(account: &BLSAccount) -> Self { + pub fn from_bls_account(account: &BLSAccount) -> Result { // Use the BLS public key as the key source let key_source = address_pool::KeySource::BLSPublic(account.bls_public_key.clone()); - let managed_type = ManagedAccountType::from_account_type( + let managed_type = match ManagedAccountType::from_account_type( account.account_type, account.network, &key_source, - ) - .unwrap_or_else(|_| { - // Fallback: create without pre-generated addresses - let no_key_source = address_pool::KeySource::NoKeySource; - ManagedAccountType::from_account_type( - account.account_type, - account.network, - &no_key_source, - ) - .expect("Should succeed with NoKeySource") - }); + ) { + Ok(managed) => managed, + Err(e @ crate::error::Error::InvalidParameter(_)) => return Err(e), + Err(_) => { + // Fallback: create without pre-generated addresses + let no_key_source = address_pool::KeySource::NoKeySource; + ManagedAccountType::from_account_type( + account.account_type, + account.network, + &no_key_source, + )? + } + }; - Self::new(managed_type, account.network, account.is_watch_only) + Ok(Self::new(managed_type, account.network, account.is_watch_only)) } /// Create a ManagedAccount from an EdDSA Account @@ -981,14 +993,16 @@ impl ManagedCoreAccount { return Err("Expected BLS public key but got different key type"); }; - addresses.mark_index_used(info.index); - use dashcore::blsful::{Bls12381G2Impl, PublicKey, SerializationFormat}; - PublicKey::::from_bytes_with_mode( + let public_key = PublicKey::::from_bytes_with_mode( &pub_key_bytes, SerializationFormat::Modern, ) - .map_err(|_| "Failed to deserialize BLS public key") + .map_err(|_| "Failed to deserialize BLS public key")?; + + addresses.mark_index_used(info.index); + + Ok(public_key) } /// Generate the next BLS operator key (only for ProviderOperatorKeys accounts) diff --git a/key-wallet/src/transaction_checking/transaction_router/mod.rs b/key-wallet/src/transaction_checking/transaction_router/mod.rs index 1ef8e59e6..6b15d3d73 100644 --- a/key-wallet/src/transaction_checking/transaction_router/mod.rs +++ b/key-wallet/src/transaction_checking/transaction_router/mod.rs @@ -167,13 +167,24 @@ impl TransactionRouter { } } -/// Core account types that can be checked for transactions +/// Core account types that can be checked for transactions. /// -/// Note: Platform Payment accounts (DIP-17) and DIP-13 identity authentication -/// accounts (sub-feature 0') are NOT included here — they operate on Dash -/// Platform, not the Core chain, so the conversions from -/// [`ManagedAccountType`]/[`crate::AccountType`] explicitly return -/// [`PlatformAccountConversionError`] for those variants. +/// Platform-only account types are NOT represented here because they operate +/// on Dash Platform, not the Core chain. The two Platform-only cases differ +/// in where they live and how the conversion rejects them: +/// +/// - **Platform Payment (DIP-17)** exists on both sides: +/// [`ManagedAccountType::PlatformPayment`] and +/// [`crate::AccountType::PlatformPayment`]. `TryFrom` on either produces +/// [`PlatformAccountConversionError`]. +/// - **DIP-13 identity authentication** exists ONLY on the immutable side +/// as [`crate::AccountType::IdentityAuthenticationEcdsa`] / +/// [`crate::AccountType::IdentityAuthenticationBls`]. It intentionally +/// has no [`ManagedAccountType`] variant — these keys carry no L1 state, +/// so there is nothing to manage. Consequently +/// `TryFrom` rejects only `PlatformPayment`, while +/// `TryFrom` rejects both `PlatformPayment` and the +/// two DIP-13 identity authentication variants. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AccountTypeToCheck { StandardBIP44, diff --git a/key-wallet/src/wallet/managed_wallet_info/managed_accounts.rs b/key-wallet/src/wallet/managed_wallet_info/managed_accounts.rs index f21713c1b..61409f82d 100644 --- a/key-wallet/src/wallet/managed_wallet_info/managed_accounts.rs +++ b/key-wallet/src/wallet/managed_wallet_info/managed_accounts.rs @@ -42,7 +42,7 @@ impl ManagedAccountOperations for ManagedWalletInfo { })?; // Create the ManagedAccount from the Account - let managed_account = ManagedCoreAccount::from_account(account); + let managed_account = ManagedCoreAccount::from_account(account)?; // Check if managed account already exists if self.accounts.contains_managed_account_type(managed_account.managed_type()) { @@ -117,7 +117,7 @@ impl ManagedAccountOperations for ManagedWalletInfo { let account = Account::new(None, account_type, account_xpub, self.network)?; // Create the ManagedAccount from the Account - let managed_account = ManagedCoreAccount::from_account(&account); + let managed_account = ManagedCoreAccount::from_account(&account)?; // Check if managed account already exists if self.accounts.contains_managed_account_type(managed_account.managed_type()) { @@ -162,7 +162,7 @@ impl ManagedAccountOperations for ManagedWalletInfo { })?; // Create the ManagedAccount from the BLS Account - let managed_account = ManagedCoreAccount::from_bls_account(bls_account); + let managed_account = ManagedCoreAccount::from_bls_account(bls_account)?; // Check if managed account already exists if self.accounts.contains_managed_account_type(managed_account.managed_type()) { @@ -234,7 +234,7 @@ impl ManagedAccountOperations for ManagedWalletInfo { BLSAccount::from_public_key_bytes(None, account_type, bls_public_key, self.network)?; // Create the ManagedAccount from the BLS Account - let managed_account = ManagedCoreAccount::from_bls_account(&bls_account); + let managed_account = ManagedCoreAccount::from_bls_account(&bls_account)?; // Check if managed account already exists if self.accounts.contains_managed_account_type(managed_account.managed_type()) {