Skip to content

Commit b6ebb8b

Browse files
feat(key-wallet): add DIP-13 identity authentication accounts (ECDSA + BLS)
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<u32, Account>` and (under `bls`) `identity_authentication_bls: BTreeMap<u32, BLSAccount>`, 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) <noreply@anthropic.com>
1 parent ee1ebd9 commit b6ebb8b

15 files changed

Lines changed: 866 additions & 19 deletions

File tree

key-wallet-ffi/src/address_pool.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ fn get_managed_account_by_type<'a>(
4545
collection.identity_topup_not_bound.as_ref()
4646
}
4747
AccountType::IdentityInvitation => collection.identity_invitation.as_ref(),
48+
AccountType::IdentityAuthenticationEcdsa {
49+
identity_index,
50+
} => collection.identity_authentication_ecdsa.get(identity_index),
51+
AccountType::IdentityAuthenticationBls {
52+
identity_index,
53+
} => collection.identity_authentication_bls.get(identity_index),
4854
AccountType::AssetLockAddressTopUp => collection.asset_lock_address_topup.as_ref(),
4955
AccountType::AssetLockShieldedAddressTopUp => {
5056
collection.asset_lock_shielded_address_topup.as_ref()
@@ -98,6 +104,12 @@ fn get_managed_account_by_type_mut<'a>(
98104
collection.identity_topup_not_bound.as_mut()
99105
}
100106
AccountType::IdentityInvitation => collection.identity_invitation.as_mut(),
107+
AccountType::IdentityAuthenticationEcdsa {
108+
identity_index,
109+
} => collection.identity_authentication_ecdsa.get_mut(identity_index),
110+
AccountType::IdentityAuthenticationBls {
111+
identity_index,
112+
} => collection.identity_authentication_bls.get_mut(identity_index),
101113
AccountType::AssetLockAddressTopUp => collection.asset_lock_address_topup.as_mut(),
102114
AccountType::AssetLockShieldedAddressTopUp => {
103115
collection.asset_lock_shielded_address_topup.as_mut()

key-wallet-ffi/src/managed_account.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,12 @@ pub unsafe extern "C" fn managed_wallet_get_account(
247247
managed_collection.identity_topup_not_bound.as_ref()
248248
}
249249
AccountType::IdentityInvitation => managed_collection.identity_invitation.as_ref(),
250+
AccountType::IdentityAuthenticationEcdsa {
251+
identity_index,
252+
} => managed_collection.identity_authentication_ecdsa.get(&identity_index),
253+
AccountType::IdentityAuthenticationBls {
254+
identity_index,
255+
} => managed_collection.identity_authentication_bls.get(&identity_index),
250256
AccountType::AssetLockAddressTopUp => {
251257
managed_collection.asset_lock_address_topup.as_ref()
252258
}
@@ -564,6 +570,12 @@ pub unsafe extern "C" fn managed_core_account_get_account_type(
564570
FFIAccountType::IdentityTopUpNotBoundToIdentity
565571
}
566572
AccountType::IdentityInvitation => FFIAccountType::IdentityInvitation,
573+
AccountType::IdentityAuthenticationEcdsa {
574+
..
575+
} => FFIAccountType::IdentityAuthenticationEcdsa,
576+
AccountType::IdentityAuthenticationBls {
577+
..
578+
} => FFIAccountType::IdentityAuthenticationBls,
567579
AccountType::AssetLockAddressTopUp => FFIAccountType::AssetLockAddressTopUp,
568580
AccountType::AssetLockShieldedAddressTopUp => FFIAccountType::AssetLockShieldedAddressTopUp,
569581
AccountType::ProviderVotingKeys => FFIAccountType::ProviderVotingKeys,
@@ -1167,6 +1179,14 @@ pub unsafe extern "C" fn managed_core_account_get_address_pool(
11671179
addresses,
11681180
..
11691181
} => addresses,
1182+
ManagedAccountType::IdentityAuthenticationEcdsa {
1183+
addresses,
1184+
..
1185+
} => addresses,
1186+
ManagedAccountType::IdentityAuthenticationBls {
1187+
addresses,
1188+
..
1189+
} => addresses,
11701190
};
11711191

11721192
let ffi_pool = FFIAddressPool {

key-wallet-ffi/src/types.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,12 @@ pub enum FFIAccountType {
235235
AssetLockAddressTopUp = 14,
236236
/// Asset lock shielded address top-up funding (subfeature 5)
237237
AssetLockShieldedAddressTopUp = 15,
238+
/// Per-identity ECDSA authentication keys (DIP-13, sub-feature 0', key type 0').
239+
/// Path prefix: `m/9'/coin_type'/5'/0'/0'/identity_index'`.
240+
IdentityAuthenticationEcdsa = 16,
241+
/// Per-identity BLS authentication keys (DIP-13, sub-feature 0', key type 1').
242+
/// Path prefix: `m/9'/coin_type'/5'/0'/1'/identity_index'`.
243+
IdentityAuthenticationBls = 17,
238244
}
239245

240246
impl FFIAccountType {
@@ -273,6 +279,18 @@ impl FFIAccountType {
273279
FFIAccountType::ProviderOwnerKeys => key_wallet::AccountType::ProviderOwnerKeys,
274280
FFIAccountType::ProviderOperatorKeys => key_wallet::AccountType::ProviderOperatorKeys,
275281
FFIAccountType::ProviderPlatformKeys => key_wallet::AccountType::ProviderPlatformKeys,
282+
// DIP-13 authentication accounts use the provided `index` as
283+
// `identity_index` (the hardened child at path level 6).
284+
FFIAccountType::IdentityAuthenticationEcdsa => {
285+
key_wallet::AccountType::IdentityAuthenticationEcdsa {
286+
identity_index: index,
287+
}
288+
}
289+
FFIAccountType::IdentityAuthenticationBls => {
290+
key_wallet::AccountType::IdentityAuthenticationBls {
291+
identity_index: index,
292+
}
293+
}
276294
// DashPay variants require additional identity IDs (user_identity_id and friend_identity_id)
277295
// that are not part of the current FFI API. These types cannot be constructed via this
278296
// conversion path. Attempting to use them is a programming error.
@@ -366,6 +384,12 @@ impl FFIAccountType {
366384
key_wallet::AccountType::ProviderPlatformKeys => {
367385
(FFIAccountType::ProviderPlatformKeys, 0, None)
368386
}
387+
key_wallet::AccountType::IdentityAuthenticationEcdsa {
388+
identity_index,
389+
} => (FFIAccountType::IdentityAuthenticationEcdsa, *identity_index, None),
390+
key_wallet::AccountType::IdentityAuthenticationBls {
391+
identity_index,
392+
} => (FFIAccountType::IdentityAuthenticationBls, *identity_index, None),
369393
key_wallet::AccountType::DashpayReceivingFunds {
370394
index,
371395
user_identity_id,

key-wallet/src/account/account_collection.rs

Lines changed: 97 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ pub struct AccountCollection {
5858
pub identity_topup_not_bound: Option<Account>,
5959
/// Identity invitation account (optional)
6060
pub identity_invitation: Option<Account>,
61+
/// Per-identity ECDSA authentication accounts (DIP-13, sub-feature 0',
62+
/// key type 0'), keyed by `identity_index`. Platform-only — these carry no
63+
/// L1 UTXOs.
64+
pub identity_authentication_ecdsa: BTreeMap<u32, Account>,
65+
/// Per-identity BLS authentication accounts (DIP-13, sub-feature 0',
66+
/// key type 1'), keyed by `identity_index`. Platform-only.
67+
#[cfg(feature = "bls")]
68+
pub identity_authentication_bls: BTreeMap<u32, BLSAccount>,
6169
/// Asset lock address top-up account (optional)
6270
pub asset_lock_address_topup: Option<Account>,
6371
/// Asset lock shielded address top-up account (optional)
@@ -91,6 +99,9 @@ impl AccountCollection {
9199
identity_topup: BTreeMap::new(),
92100
identity_topup_not_bound: None,
93101
identity_invitation: None,
102+
identity_authentication_ecdsa: BTreeMap::new(),
103+
#[cfg(feature = "bls")]
104+
identity_authentication_bls: BTreeMap::new(),
94105
asset_lock_address_topup: None,
95106
asset_lock_shielded_address_topup: None,
96107
provider_voting_keys: None,
@@ -141,6 +152,18 @@ impl AccountCollection {
141152
AccountType::IdentityInvitation => {
142153
self.identity_invitation = Some(account);
143154
}
155+
AccountType::IdentityAuthenticationEcdsa {
156+
identity_index,
157+
} => {
158+
self.identity_authentication_ecdsa.insert(*identity_index, account);
159+
}
160+
AccountType::IdentityAuthenticationBls {
161+
..
162+
} => {
163+
return Err(
164+
"IdentityAuthenticationBls requires BLSAccount, use insert_bls_account",
165+
);
166+
}
144167
AccountType::AssetLockAddressTopUp => {
145168
self.asset_lock_address_topup = Some(account);
146169
}
@@ -197,14 +220,29 @@ impl AccountCollection {
197220
Ok(())
198221
}
199222

200-
/// Insert a BLS account for provider operator keys
223+
/// Insert a BLS account for provider operator keys or identity
224+
/// authentication.
225+
///
226+
/// Accepts [`AccountType::ProviderOperatorKeys`] or
227+
/// [`AccountType::IdentityAuthenticationBls`]. Rejects any other account
228+
/// type.
201229
#[cfg(feature = "bls")]
202230
pub fn insert_bls_account(&mut self, account: BLSAccount) -> Result<(), &'static str> {
203-
if !matches!(account.account_type, AccountType::ProviderOperatorKeys) {
204-
return Err("BLS account must have ProviderOperatorKeys type");
231+
match account.account_type {
232+
AccountType::ProviderOperatorKeys => {
233+
self.provider_operator_keys = Some(account);
234+
Ok(())
235+
}
236+
AccountType::IdentityAuthenticationBls {
237+
identity_index,
238+
} => {
239+
self.identity_authentication_bls.insert(identity_index, account);
240+
Ok(())
241+
}
242+
_ => {
243+
Err("BLS account must have ProviderOperatorKeys or IdentityAuthenticationBls type")
244+
}
205245
}
206-
self.provider_operator_keys = Some(account);
207-
Ok(())
208246
}
209247

210248
/// Insert an EdDSA account for provider platform keys
@@ -242,6 +280,17 @@ impl AccountCollection {
242280
} => self.identity_topup.contains_key(registration_index),
243281
AccountType::IdentityTopUpNotBoundToIdentity => self.identity_topup_not_bound.is_some(),
244282
AccountType::IdentityInvitation => self.identity_invitation.is_some(),
283+
AccountType::IdentityAuthenticationEcdsa {
284+
identity_index,
285+
} => self.identity_authentication_ecdsa.contains_key(identity_index),
286+
#[cfg(feature = "bls")]
287+
AccountType::IdentityAuthenticationBls {
288+
identity_index,
289+
} => self.identity_authentication_bls.contains_key(identity_index),
290+
#[cfg(not(feature = "bls"))]
291+
AccountType::IdentityAuthenticationBls {
292+
..
293+
} => false,
245294
AccountType::AssetLockAddressTopUp => self.asset_lock_address_topup.is_some(),
246295
AccountType::AssetLockShieldedAddressTopUp => {
247296
self.asset_lock_shielded_address_topup.is_some()
@@ -315,6 +364,12 @@ impl AccountCollection {
315364
} => self.identity_topup.get(&registration_index),
316365
AccountType::IdentityTopUpNotBoundToIdentity => self.identity_topup_not_bound.as_ref(),
317366
AccountType::IdentityInvitation => self.identity_invitation.as_ref(),
367+
AccountType::IdentityAuthenticationEcdsa {
368+
identity_index,
369+
} => self.identity_authentication_ecdsa.get(&identity_index),
370+
AccountType::IdentityAuthenticationBls {
371+
..
372+
} => None, // BLSAccount, use bls_account_of_type
318373
AccountType::AssetLockAddressTopUp => self.asset_lock_address_topup.as_ref(),
319374
AccountType::AssetLockShieldedAddressTopUp => {
320375
self.asset_lock_shielded_address_topup.as_ref()
@@ -382,6 +437,12 @@ impl AccountCollection {
382437
} => self.identity_topup.get_mut(&registration_index),
383438
AccountType::IdentityTopUpNotBoundToIdentity => self.identity_topup_not_bound.as_mut(),
384439
AccountType::IdentityInvitation => self.identity_invitation.as_mut(),
440+
AccountType::IdentityAuthenticationEcdsa {
441+
identity_index,
442+
} => self.identity_authentication_ecdsa.get_mut(&identity_index),
443+
AccountType::IdentityAuthenticationBls {
444+
..
445+
} => None, // BLSAccount, use bls_account_of_type_mut
385446
AccountType::AssetLockAddressTopUp => self.asset_lock_address_topup.as_mut(),
386447
AccountType::AssetLockShieldedAddressTopUp => {
387448
self.asset_lock_shielded_address_topup.as_mut()
@@ -449,6 +510,8 @@ impl AccountCollection {
449510
accounts.push(account);
450511
}
451512

513+
accounts.extend(self.identity_authentication_ecdsa.values());
514+
452515
if let Some(account) = &self.asset_lock_address_topup {
453516
accounts.push(account);
454517
}
@@ -497,6 +560,8 @@ impl AccountCollection {
497560
accounts.push(account);
498561
}
499562

563+
accounts.extend(self.identity_authentication_ecdsa.values_mut());
564+
500565
if let Some(account) = &mut self.asset_lock_address_topup {
501566
accounts.push(account);
502567
}
@@ -523,23 +588,37 @@ impl AccountCollection {
523588
accounts
524589
}
525590

526-
/// Get the BLS account (provider operator keys)
591+
/// Get a BLS account by type.
592+
///
593+
/// Supports [`AccountType::ProviderOperatorKeys`] and
594+
/// [`AccountType::IdentityAuthenticationBls`] — returns `None` for other
595+
/// types.
527596
#[cfg(feature = "bls")]
528597
pub fn bls_account_of_type(&self, account_type: AccountType) -> Option<&BLSAccount> {
529598
match account_type {
530599
AccountType::ProviderOperatorKeys => self.provider_operator_keys.as_ref(),
600+
AccountType::IdentityAuthenticationBls {
601+
identity_index,
602+
} => self.identity_authentication_bls.get(&identity_index),
531603
_ => None,
532604
}
533605
}
534606

535-
/// Get the BLS account mutably (provider operator keys)
607+
/// Get a BLS account by type mutably.
608+
///
609+
/// Supports [`AccountType::ProviderOperatorKeys`] and
610+
/// [`AccountType::IdentityAuthenticationBls`] — returns `None` for other
611+
/// types.
536612
#[cfg(feature = "bls")]
537613
pub fn bls_account_of_type_mut(
538614
&mut self,
539615
account_type: AccountType,
540616
) -> Option<&mut BLSAccount> {
541617
match account_type {
542618
AccountType::ProviderOperatorKeys => self.provider_operator_keys.as_mut(),
619+
AccountType::IdentityAuthenticationBls {
620+
identity_index,
621+
} => self.identity_authentication_bls.get_mut(&identity_index),
543622
_ => None,
544623
}
545624
}
@@ -575,6 +654,11 @@ impl AccountCollection {
575654
count += 1;
576655
}
577656

657+
#[cfg(feature = "bls")]
658+
{
659+
count += self.identity_authentication_bls.len();
660+
}
661+
578662
#[cfg(feature = "eddsa")]
579663
if self.provider_platform_keys.is_some() {
580664
count += 1;
@@ -605,14 +689,17 @@ impl AccountCollection {
605689
&& self.identity_topup.is_empty()
606690
&& self.identity_topup_not_bound.is_none()
607691
&& self.identity_invitation.is_none()
692+
&& self.identity_authentication_ecdsa.is_empty()
608693
&& self.asset_lock_address_topup.is_none()
609694
&& self.asset_lock_shielded_address_topup.is_none()
610695
&& self.provider_voting_keys.is_none()
611696
&& self.provider_owner_keys.is_none();
612697

613698
#[cfg(feature = "bls")]
614699
{
615-
is_empty = is_empty && self.provider_operator_keys.is_none();
700+
is_empty = is_empty
701+
&& self.provider_operator_keys.is_none()
702+
&& self.identity_authentication_bls.is_empty();
616703
}
617704

618705
#[cfg(feature = "eddsa")]
@@ -632,13 +719,15 @@ impl AccountCollection {
632719
self.identity_topup.clear();
633720
self.identity_topup_not_bound = None;
634721
self.identity_invitation = None;
722+
self.identity_authentication_ecdsa.clear();
635723
self.asset_lock_address_topup = None;
636724
self.asset_lock_shielded_address_topup = None;
637725
self.provider_voting_keys = None;
638726
self.provider_owner_keys = None;
639727
#[cfg(feature = "bls")]
640728
{
641729
self.provider_operator_keys = None;
730+
self.identity_authentication_bls.clear();
642731
}
643732
#[cfg(feature = "eddsa")]
644733
{

key-wallet/src/account/account_collection_test.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,10 @@ mod tests {
125125

126126
let result = collection.insert_bls_account(bls_account);
127127
assert!(result.is_err());
128-
assert_eq!(result.unwrap_err(), "BLS account must have ProviderOperatorKeys type");
128+
assert_eq!(
129+
result.unwrap_err(),
130+
"BLS account must have ProviderOperatorKeys or IdentityAuthenticationBls type"
131+
);
129132
}
130133

131134
#[test]

0 commit comments

Comments
 (0)