diff --git a/key-wallet-ffi/FFI_API.md b/key-wallet-ffi/FFI_API.md index b2db10a5c..8d4b462ce 100644 --- a/key-wallet-ffi/FFI_API.md +++ b/key-wallet-ffi/FFI_API.md @@ -4,7 +4,7 @@ This document provides a comprehensive reference for all FFI (Foreign Function I **Auto-generated**: This documentation is automatically generated from the source code. Do not edit manually. -**Total Functions**: 222 +**Total Functions**: 232 ## Table of Contents @@ -130,7 +130,7 @@ Functions: 57 ### Account Management -Functions: 81 +Functions: 92 | Function | Description | Module | |----------|-------------|--------| @@ -161,17 +161,28 @@ Functions: 81 | `account_collection_summary` | Get a human-readable summary of all accounts in the collection Returns a for... | account_collection | | `account_collection_summary_data` | Get structured account collection summary data Returns a struct containing a... | account_collection | | `account_collection_summary_free` | Free an account collection summary and all its allocated memory # Safety - ... | account_collection | +| `account_derive_extended_private_key_at` | Derive an extended private key from an account at a given index, using the pr... | account_derivation | +| `account_derive_extended_private_key_from_mnemonic` | Derive an extended private key from a mnemonic + optional passphrase at the g... | account_derivation | +| `account_derive_extended_private_key_from_seed` | Derive an extended private key from a raw seed buffer at the given index | account_derivation | +| `account_derive_private_key_as_wif_at` | Derive a private key from an account at a given chain/index and return as WIF... | account_derivation | +| `account_derive_private_key_at` | Derive a private key (secp256k1) from an account at a given chain/index, usin... | account_derivation | +| `account_derive_private_key_from_mnemonic` | Derive a private key from a mnemonic + optional passphrase at the given index | account_derivation | +| `account_derive_private_key_from_seed` | Derive a private key from a raw seed buffer at the given index | account_derivation | | `account_free` | Free an account handle # Safety - `account` must be a valid pointer to an F... | account | | `account_get_account_type` | Get the account type of an account # Safety - `account` must be a valid poi... | account | | `account_get_extended_public_key_as_string` | Get the extended public key of an account as a string # Safety - `account` ... | account | | `account_get_is_watch_only` | Check if an account is watch-only # Safety - `account` must be a valid poin... | account | | `account_get_network` | Get the network of an account # Safety - `account` must be a valid pointer ... | account | +| `bls_account_derive_private_key_from_mnemonic` | No description | account_derivation | +| `bls_account_derive_private_key_from_seed` | No description | account_derivation | | `bls_account_free` | No description | account | | `bls_account_get_account_type` | No description | account | | `bls_account_get_extended_public_key_as_string` | No description | account | | `bls_account_get_is_watch_only` | No description | account | | `bls_account_get_network` | No description | account | | `derivation_bip44_account_path` | Derive a BIP44 account path (m/44'/5'/account') | derivation | +| `eddsa_account_derive_private_key_from_mnemonic` | No description | account_derivation | +| `eddsa_account_derive_private_key_from_seed` | No description | account_derivation | | `eddsa_account_free` | No description | account | | `eddsa_account_get_account_type` | No description | account | | `eddsa_account_get_extended_public_key_as_string` | No description | account | @@ -255,7 +266,7 @@ Functions: 13 ### Key Management -Functions: 15 +Functions: 14 | Function | Description | Module | |----------|-------------|--------| @@ -263,7 +274,6 @@ Functions: 15 | `bip38_encrypt_private_key` | Encrypt a private key with BIP38 # Safety This function is unsafe because i... | bip38 | | `derivation_derive_private_key_from_seed` | Derive private key for a specific path from seed # Safety - `seed` must be ... | derivation | | `derivation_new_master_key` | Create a new master extended private key from seed # Safety - `seed` must b... | derivation | -| `dip9_derive_identity_key` | Derive key using DIP9 path constants for identity # Safety - `seed` must be... | derivation | | `extended_private_key_free` | Free an extended private key # Safety - `key` must be a valid pointer creat... | keys | | `extended_private_key_get_private_key` | Get the private key from an extended private key Extracts the non-extended p... | keys | | `extended_private_key_to_string` | Get extended private key as string (xprv format) Returns the extended privat... | keys | @@ -1982,6 +1992,118 @@ Free an account collection summary and all its allocated memory # Safety - `su --- +#### `account_derive_extended_private_key_at` + +```c +account_derive_extended_private_key_at(account: *const FFIAccount, master_xpriv: *const FFIExtendedPrivateKey, index: c_uint, error: *mut FFIError,) -> *mut FFIExtendedPrivateKey +``` + +**Description:** +Derive an extended private key from an account at a given index, using the provided master xpriv. Returns an opaque FFIExtendedPrivateKey pointer that must be freed with `extended_private_key_free`. Notes: - This is chain-agnostic. For accounts with internal/external chains, this returns an error. - For hardened-only account types (e.g., EdDSA), a hardened index is used. # Safety - `account` and `master_xpriv` must be valid, non-null pointers allocated by this library. - `error` must be a valid pointer to an FFIError or null. - The caller must free the returned pointer with `extended_private_key_free`. + +**Safety:** +- `account` and `master_xpriv` must be valid, non-null pointers allocated by this library. - `error` must be a valid pointer to an FFIError or null. - The caller must free the returned pointer with `extended_private_key_free`. + +**Module:** `account_derivation` + +--- + +#### `account_derive_extended_private_key_from_mnemonic` + +```c +account_derive_extended_private_key_from_mnemonic(account: *const FFIAccount, mnemonic: *const c_char, passphrase: *const c_char, index: c_uint, error: *mut FFIError,) -> *mut FFIExtendedPrivateKey +``` + +**Description:** +Derive an extended private key from a mnemonic + optional passphrase at the given index. Returns an opaque FFIExtendedPrivateKey pointer that must be freed with `extended_private_key_free`. # Safety - `account` must be a valid pointer to an FFIAccount - `mnemonic` must be a valid, null-terminated C string - `passphrase` may be null; if not null, must be a valid C string - `error` must be a valid pointer to an FFIError or null + +**Safety:** +- `account` must be a valid pointer to an FFIAccount - `mnemonic` must be a valid, null-terminated C string - `passphrase` may be null; if not null, must be a valid C string - `error` must be a valid pointer to an FFIError or null + +**Module:** `account_derivation` + +--- + +#### `account_derive_extended_private_key_from_seed` + +```c +account_derive_extended_private_key_from_seed(account: *const FFIAccount, seed: *const u8, seed_len: usize, index: c_uint, error: *mut FFIError,) -> *mut FFIExtendedPrivateKey +``` + +**Description:** +Derive an extended private key from a raw seed buffer at the given index. Returns an opaque FFIExtendedPrivateKey pointer that must be freed with `extended_private_key_free`. # Safety - `account` must be a valid pointer to an FFIAccount - `seed` must point to a valid buffer of length `seed_len` - `error` must be a valid pointer to an FFIError or null + +**Safety:** +- `account` must be a valid pointer to an FFIAccount - `seed` must point to a valid buffer of length `seed_len` - `error` must be a valid pointer to an FFIError or null + +**Module:** `account_derivation` + +--- + +#### `account_derive_private_key_as_wif_at` + +```c +account_derive_private_key_as_wif_at(account: *const FFIAccount, master_xpriv: *const FFIExtendedPrivateKey, index: c_uint, error: *mut FFIError,) -> *mut c_char +``` + +**Description:** +Derive a private key from an account at a given chain/index and return as WIF string. Caller must free the returned string with `string_free`. # Safety - `account` and `master_xpriv` must be valid pointers allocated by this library - `error` must be a valid pointer to an FFIError or null + +**Safety:** +- `account` and `master_xpriv` must be valid pointers allocated by this library - `error` must be a valid pointer to an FFIError or null + +**Module:** `account_derivation` + +--- + +#### `account_derive_private_key_at` + +```c +account_derive_private_key_at(account: *const FFIAccount, master_xpriv: *const FFIExtendedPrivateKey, index: c_uint, error: *mut FFIError,) -> *mut FFIPrivateKey +``` + +**Description:** +Derive a private key (secp256k1) from an account at a given chain/index, using the provided master xpriv. Returns an opaque FFIPrivateKey pointer that must be freed with `private_key_free`. # Safety - `account` and `master_xpriv` must be valid pointers allocated by this library - `error` must be a valid pointer to an FFIError or null + +**Safety:** +- `account` and `master_xpriv` must be valid pointers allocated by this library - `error` must be a valid pointer to an FFIError or null + +**Module:** `account_derivation` + +--- + +#### `account_derive_private_key_from_mnemonic` + +```c +account_derive_private_key_from_mnemonic(account: *const FFIAccount, mnemonic: *const c_char, passphrase: *const c_char, index: c_uint, error: *mut FFIError,) -> *mut FFIPrivateKey +``` + +**Description:** +Derive a private key from a mnemonic + optional passphrase at the given index. Returns an opaque FFIPrivateKey pointer that must be freed with `private_key_free`. # Safety - `account` must be a valid pointer to an FFIAccount - `mnemonic` must be a valid, null-terminated C string - `passphrase` may be null; if not null, must be a valid C string - `error` must be a valid pointer to an FFIError or null + +**Safety:** +- `account` must be a valid pointer to an FFIAccount - `mnemonic` must be a valid, null-terminated C string - `passphrase` may be null; if not null, must be a valid C string - `error` must be a valid pointer to an FFIError or null + +**Module:** `account_derivation` + +--- + +#### `account_derive_private_key_from_seed` + +```c +account_derive_private_key_from_seed(account: *const FFIAccount, seed: *const u8, seed_len: usize, index: c_uint, error: *mut FFIError,) -> *mut FFIPrivateKey +``` + +**Description:** +Derive a private key from a raw seed buffer at the given index. Returns an opaque FFIPrivateKey pointer that must be freed with `private_key_free`. # Safety - `account` must be a valid pointer to an FFIAccount - `seed` must point to a valid buffer of length `seed_len` - `error` must be a valid pointer to an FFIError or null + +**Safety:** +- `account` must be a valid pointer to an FFIAccount - `seed` must point to a valid buffer of length `seed_len` - `error` must be a valid pointer to an FFIError or null + +**Module:** `account_derivation` + +--- + #### `account_free` ```c @@ -2062,6 +2184,26 @@ Get the network of an account # Safety - `account` must be a valid pointer to --- +#### `bls_account_derive_private_key_from_mnemonic` + +```c +bls_account_derive_private_key_from_mnemonic(account: *const FFIBLSAccount, mnemonic: *const c_char, passphrase: *const c_char, index: c_uint, error: *mut FFIError,) -> *mut c_char +``` + +**Module:** `account_derivation` + +--- + +#### `bls_account_derive_private_key_from_seed` + +```c +bls_account_derive_private_key_from_seed(account: *const FFIBLSAccount, seed: *const u8, seed_len: usize, index: c_uint, error: *mut FFIError,) -> *mut c_char +``` + +**Module:** `account_derivation` + +--- + #### `bls_account_free` ```c @@ -2125,6 +2267,26 @@ Derive a BIP44 account path (m/44'/5'/account') --- +#### `eddsa_account_derive_private_key_from_mnemonic` + +```c +eddsa_account_derive_private_key_from_mnemonic(account: *const FFIEdDSAAccount, mnemonic: *const c_char, passphrase: *const c_char, index: c_uint, error: *mut FFIError,) -> *mut c_char +``` + +**Module:** `account_derivation` + +--- + +#### `eddsa_account_derive_private_key_from_seed` + +```c +eddsa_account_derive_private_key_from_seed(account: *const FFIEdDSAAccount, seed: *const u8, seed_len: usize, index: c_uint, error: *mut FFIError,) -> *mut c_char +``` + +**Module:** `account_derivation` + +--- + #### `eddsa_account_free` ```c @@ -3218,22 +3380,6 @@ Create a new master extended private key from seed # Safety - `seed` must be a --- -#### `dip9_derive_identity_key` - -```c -dip9_derive_identity_key(seed: *const u8, seed_len: usize, network: FFINetwork, identity_index: c_uint, key_index: c_uint, key_type: FFIDerivationPathType, error: *mut FFIError,) -> *mut FFIExtendedPrivKey -``` - -**Description:** -Derive key using DIP9 path constants for identity # Safety - `seed` must be a valid pointer to a byte array of `seed_len` length - `error` must be a valid pointer to an FFIError structure or null - The caller must ensure the seed pointer remains valid for the duration of this call - -**Safety:** -- `seed` must be a valid pointer to a byte array of `seed_len` length - `error` must be a valid pointer to an FFIError structure or null - The caller must ensure the seed pointer remains valid for the duration of this call - -**Module:** `derivation` - ---- - #### `extended_private_key_free` ```c diff --git a/key-wallet-ffi/include/key_wallet_ffi.h b/key-wallet-ffi/include/key_wallet_ffi.h index d04985444..ae35cbaea 100644 --- a/key-wallet-ffi/include/key_wallet_ffi.h +++ b/key-wallet-ffi/include/key_wallet_ffi.h @@ -126,29 +126,6 @@ typedef enum { SINGLE = 2, } FFIAddressPoolType; -/* - Derivation path type for DIP9 - */ -typedef enum { - PATH_UNKNOWN = 0, - PATH_BIP32 = 1, - PATH_BIP44 = 2, - PATH_BLOCKCHAIN_IDENTITIES = 3, - PATH_PROVIDER_FUNDS = 4, - PATH_PROVIDER_VOTING_KEYS = 5, - PATH_PROVIDER_OPERATOR_KEYS = 6, - PATH_PROVIDER_OWNER_KEYS = 7, - PATH_CONTACT_BASED_FUNDS = 8, - PATH_CONTACT_BASED_FUNDS_ROOT = 9, - PATH_CONTACT_BASED_FUNDS_EXTERNAL = 10, - PATH_BLOCKCHAIN_IDENTITY_CREDIT_REGISTRATION_FUNDING = 11, - PATH_BLOCKCHAIN_IDENTITY_CREDIT_TOPUP_FUNDING = 12, - PATH_BLOCKCHAIN_IDENTITY_CREDIT_INVITATION_FUNDING = 13, - PATH_PROVIDER_PLATFORM_NODE_KEYS = 14, - PATH_COIN_JOIN = 15, - PATH_ROOT = 255, -} FFIDerivationPathType; - /* FFI Error code */ @@ -1606,6 +1583,224 @@ void address_info_array_free(FFIAddressInfo **infos, size_t count) ; +/* + Derive an extended private key from an account at a given index, using the provided master xpriv. + + Returns an opaque FFIExtendedPrivateKey pointer that must be freed with `extended_private_key_free`. + + Notes: + - This is chain-agnostic. For accounts with internal/external chains, this returns an error. + - For hardened-only account types (e.g., EdDSA), a hardened index is used. + + # Safety + - `account` and `master_xpriv` must be valid, non-null pointers allocated by this library. + - `error` must be a valid pointer to an FFIError or null. + - The caller must free the returned pointer with `extended_private_key_free`. + */ + +FFIExtendedPrivateKey *account_derive_extended_private_key_at(const FFIAccount *account, + const FFIExtendedPrivateKey *master_xpriv, + unsigned int index, + FFIError *error) +; + +/* + Derive a BLS private key from a raw seed buffer at the given index. + + Returns a newly allocated hex string of the 32-byte private key. The caller must free + it with `string_free`. + + Notes: + - Uses the account's network for master key creation. + - Chain-agnostic; may return an error for accounts with internal/external chains. + + # Safety + - `account` must be a valid, non-null pointer to an `FFIBLSAccount` (only when `bls` feature is enabled). + - `seed` must point to a readable buffer of length `seed_len` (1..=64 bytes expected). + - `error` must be a valid pointer to an FFIError or null. + - Returned string must be freed with `string_free`. + */ + +char *bls_account_derive_private_key_from_seed(const FFIBLSAccount *account, + const uint8_t *seed, + size_t seed_len, + unsigned int index, + FFIError *error) +; + +/* + Derive a BLS private key from a mnemonic + optional passphrase at the given index. + + Returns a newly allocated hex string of the 32-byte private key. The caller must free + it with `string_free`. + + Notes: + - Uses the English wordlist for parsing the mnemonic. + - Chain-agnostic; may return an error for accounts with internal/external chains. + + # Safety + - `account` must be a valid, non-null pointer to an `FFIBLSAccount` (only when `bls` feature is enabled). + - `mnemonic` must be a valid, null-terminated UTF-8 C string. + - `passphrase` may be null; if not null, must be a valid UTF-8 C string. + - `error` must be a valid pointer to an FFIError or null. + - Returned string must be freed with `string_free`. + */ + +char *bls_account_derive_private_key_from_mnemonic(const FFIBLSAccount *account, + const char *mnemonic, + const char *passphrase, + unsigned int index, + FFIError *error) +; + +/* + Derive an EdDSA (ed25519) private key from a raw seed buffer at the given index. + + Returns a newly allocated hex string of the 32-byte private key. The caller must free + it with `string_free`. + + Notes: + - EdDSA only supports hardened derivation; the index will be used accordingly. + - Chain-agnostic; EdDSA accounts typically do not have internal/external split. + + # Safety + - `account` must be a valid, non-null pointer to an `FFIEdDSAAccount` (only when `eddsa` feature is enabled). + - `seed` must point to a readable buffer of length `seed_len` (1..=64 bytes expected). + - `error` must be a valid pointer to an FFIError or null. + - Returned string must be freed with `string_free`. + */ + +char *eddsa_account_derive_private_key_from_seed(const FFIEdDSAAccount *account, + const uint8_t *seed, + size_t seed_len, + unsigned int index, + FFIError *error) +; + +/* + Derive an EdDSA (ed25519) private key from a mnemonic + optional passphrase at the given index. + + Returns a newly allocated hex string of the 32-byte private key. The caller must free + it with `string_free`. + + Notes: + - Uses the English wordlist for parsing the mnemonic. + + # Safety + - `account` must be a valid, non-null pointer to an `FFIEdDSAAccount` (only when `eddsa` feature is enabled). + - `mnemonic` must be a valid, null-terminated UTF-8 C string. + - `passphrase` may be null; if not null, must be a valid UTF-8 C string. + - `error` must be a valid pointer to an FFIError or null. + - Returned string must be freed with `string_free`. + */ + +char *eddsa_account_derive_private_key_from_mnemonic(const FFIEdDSAAccount *account, + const char *mnemonic, + const char *passphrase, + unsigned int index, + FFIError *error) +; + +/* + Derive a private key (secp256k1) from an account at a given chain/index, using the provided master xpriv. + Returns an opaque FFIPrivateKey pointer that must be freed with `private_key_free`. + + # Safety + - `account` and `master_xpriv` must be valid pointers allocated by this library + - `error` must be a valid pointer to an FFIError or null + */ + +FFIPrivateKey *account_derive_private_key_at(const FFIAccount *account, + const FFIExtendedPrivateKey *master_xpriv, + unsigned int index, + FFIError *error) +; + +/* + Derive a private key from an account at a given chain/index and return as WIF string. + Caller must free the returned string with `string_free`. + + # Safety + - `account` and `master_xpriv` must be valid pointers allocated by this library + - `error` must be a valid pointer to an FFIError or null + */ + +char *account_derive_private_key_as_wif_at(const FFIAccount *account, + const FFIExtendedPrivateKey *master_xpriv, + unsigned int index, + FFIError *error) +; + +/* + Derive an extended private key from a raw seed buffer at the given index. + Returns an opaque FFIExtendedPrivateKey pointer that must be freed with `extended_private_key_free`. + + # Safety + - `account` must be a valid pointer to an FFIAccount + - `seed` must point to a valid buffer of length `seed_len` + - `error` must be a valid pointer to an FFIError or null + */ + +FFIExtendedPrivateKey *account_derive_extended_private_key_from_seed(const FFIAccount *account, + const uint8_t *seed, + size_t seed_len, + unsigned int index, + FFIError *error) +; + +/* + Derive a private key from a raw seed buffer at the given index. + Returns an opaque FFIPrivateKey pointer that must be freed with `private_key_free`. + + # Safety + - `account` must be a valid pointer to an FFIAccount + - `seed` must point to a valid buffer of length `seed_len` + - `error` must be a valid pointer to an FFIError or null + */ + +FFIPrivateKey *account_derive_private_key_from_seed(const FFIAccount *account, + const uint8_t *seed, + size_t seed_len, + unsigned int index, + FFIError *error) +; + +/* + Derive an extended private key from a mnemonic + optional passphrase at the given index. + Returns an opaque FFIExtendedPrivateKey pointer that must be freed with `extended_private_key_free`. + + # Safety + - `account` must be a valid pointer to an FFIAccount + - `mnemonic` must be a valid, null-terminated C string + - `passphrase` may be null; if not null, must be a valid C string + - `error` must be a valid pointer to an FFIError or null + */ + +FFIExtendedPrivateKey *account_derive_extended_private_key_from_mnemonic(const FFIAccount *account, + const char *mnemonic, + const char *passphrase, + unsigned int index, + FFIError *error) +; + +/* + Derive a private key from a mnemonic + optional passphrase at the given index. + Returns an opaque FFIPrivateKey pointer that must be freed with `private_key_free`. + + # Safety + - `account` must be a valid pointer to an FFIAccount + - `mnemonic` must be a valid, null-terminated C string + - `passphrase` may be null; if not null, must be a valid C string + - `error` must be a valid pointer to an FFIError or null + */ + +FFIPrivateKey *account_derive_private_key_from_mnemonic(const FFIAccount *account, + const char *mnemonic, + const char *passphrase, + unsigned int index, + FFIError *error) +; + /* Create a new master extended private key from seed @@ -1791,25 +1986,6 @@ bool derivation_xpub_fingerprint(const FFIExtendedPubKey *xpub, */ void derivation_string_free(char *s) ; -/* - Derive key using DIP9 path constants for identity - - # Safety - - - `seed` must be a valid pointer to a byte array of `seed_len` length - - `error` must be a valid pointer to an FFIError structure or null - - The caller must ensure the seed pointer remains valid for the duration of this call - */ - -FFIExtendedPrivKey *dip9_derive_identity_key(const uint8_t *seed, - size_t seed_len, - FFINetwork network, - unsigned int identity_index, - unsigned int key_index, - FFIDerivationPathType key_type, - FFIError *error) -; - /* Derive an address from a private key diff --git a/key-wallet-ffi/src/account_derivation.rs b/key-wallet-ffi/src/account_derivation.rs new file mode 100644 index 000000000..1bd7b86ec --- /dev/null +++ b/key-wallet-ffi/src/account_derivation.rs @@ -0,0 +1,701 @@ +//! Account-level derivation functions exposed over FFI + +use crate::account::FFIAccount; +#[cfg(feature = "bls")] +use crate::account::FFIBLSAccount; +#[cfg(feature = "eddsa")] +use crate::account::FFIEdDSAAccount; +use crate::error::{FFIError, FFIErrorCode}; +use crate::keys::{FFIExtendedPrivateKey, FFIPrivateKey}; +use key_wallet::account::derivation::AccountDerivation; +use key_wallet::account::AccountTrait; +use std::ffi::CString; +use std::os::raw::{c_char, c_uint}; +use std::ptr; + +// No extra FFI enum for chain selection; account semantics decide path. + +/// Derive an extended private key from an account at a given index, using the provided master xpriv. +/// +/// Returns an opaque FFIExtendedPrivateKey pointer that must be freed with `extended_private_key_free`. +/// +/// Notes: +/// - This is chain-agnostic. For accounts with internal/external chains, this returns an error. +/// - For hardened-only account types (e.g., EdDSA), a hardened index is used. +/// +/// # Safety +/// - `account` and `master_xpriv` must be valid, non-null pointers allocated by this library. +/// - `error` must be a valid pointer to an FFIError or null. +/// - The caller must free the returned pointer with `extended_private_key_free`. +#[no_mangle] +pub unsafe extern "C" fn account_derive_extended_private_key_at( + account: *const FFIAccount, + master_xpriv: *const FFIExtendedPrivateKey, + index: c_uint, + error: *mut FFIError, +) -> *mut FFIExtendedPrivateKey { + if account.is_null() || master_xpriv.is_null() { + FFIError::set_error(error, FFIErrorCode::InvalidInput, "Null pointer provided".to_string()); + return ptr::null_mut(); + } + + let account = &*account; + let master_xpriv = &*master_xpriv; + + if account.inner().is_watch_only() { + FFIError::set_error( + error, + FFIErrorCode::WalletError, + "Account is watch-only; private derivation not allowed".to_string(), + ); + return ptr::null_mut(); + } + + match account.inner().derive_from_master_xpriv_extended_xpriv_at(master_xpriv.inner(), index) { + Ok(derived) => { + FFIError::set_success(error); + Box::into_raw(Box::new(FFIExtendedPrivateKey::from_inner(derived))) + } + Err(e) => { + FFIError::set_error( + error, + FFIErrorCode::WalletError, + format!("Failed to derive extended private key: {:?}", e), + ); + ptr::null_mut() + } + } +} + +// ========================= BLS (feature = "bls") ========================= +/// Derive a BLS private key from a raw seed buffer at the given index. +/// +/// Returns a newly allocated hex string of the 32-byte private key. The caller must free +/// it with `string_free`. +/// +/// Notes: +/// - Uses the account's network for master key creation. +/// - Chain-agnostic; may return an error for accounts with internal/external chains. +/// +/// # Safety +/// - `account` must be a valid, non-null pointer to an `FFIBLSAccount` (only when `bls` feature is enabled). +/// - `seed` must point to a readable buffer of length `seed_len` (1..=64 bytes expected). +/// - `error` must be a valid pointer to an FFIError or null. +/// - Returned string must be freed with `string_free`. +#[cfg(feature = "bls")] +#[no_mangle] +pub unsafe extern "C" fn bls_account_derive_private_key_from_seed( + account: *const FFIBLSAccount, + seed: *const u8, + seed_len: usize, + index: c_uint, + error: *mut FFIError, +) -> *mut c_char { + if account.is_null() || seed.is_null() { + FFIError::set_error(error, FFIErrorCode::InvalidInput, "Null pointer provided".to_string()); + return ptr::null_mut(); + } + let account = &*account; + if seed_len == 0 || seed_len > 64 { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Seed length must be between 1 and 64 bytes".to_string(), + ); + return ptr::null_mut(); + } + let seed_slice = std::slice::from_raw_parts(seed, seed_len); + match account.inner().derive_from_seed_private_key_at(seed_slice, index) { + Ok(sk) => { + // Return private key bytes as hex + let hex = hex::encode(sk.to_be_bytes()); + match CString::new(hex) { + Ok(s) => { + FFIError::set_success(error); + s.into_raw() + } + Err(_) => { + FFIError::set_error( + error, + FFIErrorCode::AllocationFailed, + "Allocation failed".into(), + ); + ptr::null_mut() + } + } + } + Err(e) => { + FFIError::set_error( + error, + FFIErrorCode::WalletError, + format!("Failed to derive BLS private key from seed: {:?}", e), + ); + ptr::null_mut() + } + } +} + +/// Derive a BLS private key from a mnemonic + optional passphrase at the given index. +/// +/// Returns a newly allocated hex string of the 32-byte private key. The caller must free +/// it with `string_free`. +/// +/// Notes: +/// - Uses the English wordlist for parsing the mnemonic. +/// - Chain-agnostic; may return an error for accounts with internal/external chains. +/// +/// # Safety +/// - `account` must be a valid, non-null pointer to an `FFIBLSAccount` (only when `bls` feature is enabled). +/// - `mnemonic` must be a valid, null-terminated UTF-8 C string. +/// - `passphrase` may be null; if not null, must be a valid UTF-8 C string. +/// - `error` must be a valid pointer to an FFIError or null. +/// - Returned string must be freed with `string_free`. +#[cfg(feature = "bls")] +#[no_mangle] +pub unsafe extern "C" fn bls_account_derive_private_key_from_mnemonic( + account: *const FFIBLSAccount, + mnemonic: *const c_char, + passphrase: *const c_char, + index: c_uint, + error: *mut FFIError, +) -> *mut c_char { + if account.is_null() || mnemonic.is_null() { + FFIError::set_error(error, FFIErrorCode::InvalidInput, "Null pointer provided".to_string()); + return ptr::null_mut(); + } + let account = &*account; + let mnemonic_str = match std::ffi::CStr::from_ptr(mnemonic).to_str() { + Ok(s) => s, + Err(_) => { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Invalid mnemonic string".into(), + ); + return ptr::null_mut(); + } + }; + let passphrase_str = if passphrase.is_null() { + None + } else { + match std::ffi::CStr::from_ptr(passphrase).to_str() { + Ok(s) => Some(s), + Err(_) => { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Invalid passphrase string".into(), + ); + return ptr::null_mut(); + } + } + }; + match account.inner().derive_from_mnemonic_private_key_at( + mnemonic_str, + passphrase_str, + key_wallet::mnemonic::Language::English, + index, + ) { + Ok(sk) => { + let hex = hex::encode(sk.to_be_bytes()); + match CString::new(hex) { + Ok(s) => { + FFIError::set_success(error); + s.into_raw() + } + Err(_) => { + FFIError::set_error( + error, + FFIErrorCode::AllocationFailed, + "Allocation failed".into(), + ); + ptr::null_mut() + } + } + } + Err(e) => { + FFIError::set_error( + error, + FFIErrorCode::WalletError, + format!("Failed to derive BLS private key from mnemonic: {:?}", e), + ); + ptr::null_mut() + } + } +} + +// ========================= EdDSA (feature = "eddsa") ========================= +/// Derive an EdDSA (ed25519) private key from a raw seed buffer at the given index. +/// +/// Returns a newly allocated hex string of the 32-byte private key. The caller must free +/// it with `string_free`. +/// +/// Notes: +/// - EdDSA only supports hardened derivation; the index will be used accordingly. +/// - Chain-agnostic; EdDSA accounts typically do not have internal/external split. +/// +/// # Safety +/// - `account` must be a valid, non-null pointer to an `FFIEdDSAAccount` (only when `eddsa` feature is enabled). +/// - `seed` must point to a readable buffer of length `seed_len` (1..=64 bytes expected). +/// - `error` must be a valid pointer to an FFIError or null. +/// - Returned string must be freed with `string_free`. +#[cfg(feature = "eddsa")] +#[no_mangle] +pub unsafe extern "C" fn eddsa_account_derive_private_key_from_seed( + account: *const FFIEdDSAAccount, + seed: *const u8, + seed_len: usize, + index: c_uint, + error: *mut FFIError, +) -> *mut c_char { + if account.is_null() || seed.is_null() { + FFIError::set_error(error, FFIErrorCode::InvalidInput, "Null pointer provided".to_string()); + return ptr::null_mut(); + } + let account = &*account; + let seed_slice = std::slice::from_raw_parts(seed, seed_len); + match account.inner().derive_from_seed_private_key_at(seed_slice, index) { + Ok(sk) => { + // Return 32-byte ed25519 seed/private key as hex + let hex = hex::encode(sk.to_bytes()); + match CString::new(hex) { + Ok(s) => { + FFIError::set_success(error); + s.into_raw() + } + Err(_) => { + FFIError::set_error( + error, + FFIErrorCode::AllocationFailed, + "Allocation failed".into(), + ); + ptr::null_mut() + } + } + } + Err(e) => { + FFIError::set_error( + error, + FFIErrorCode::WalletError, + format!("Failed to derive EdDSA private key from seed: {:?}", e), + ); + ptr::null_mut() + } + } +} + +/// Derive an EdDSA (ed25519) private key from a mnemonic + optional passphrase at the given index. +/// +/// Returns a newly allocated hex string of the 32-byte private key. The caller must free +/// it with `string_free`. +/// +/// Notes: +/// - Uses the English wordlist for parsing the mnemonic. +/// +/// # Safety +/// - `account` must be a valid, non-null pointer to an `FFIEdDSAAccount` (only when `eddsa` feature is enabled). +/// - `mnemonic` must be a valid, null-terminated UTF-8 C string. +/// - `passphrase` may be null; if not null, must be a valid UTF-8 C string. +/// - `error` must be a valid pointer to an FFIError or null. +/// - Returned string must be freed with `string_free`. +#[cfg(feature = "eddsa")] +#[no_mangle] +pub unsafe extern "C" fn eddsa_account_derive_private_key_from_mnemonic( + account: *const FFIEdDSAAccount, + mnemonic: *const c_char, + passphrase: *const c_char, + index: c_uint, + error: *mut FFIError, +) -> *mut c_char { + if account.is_null() || mnemonic.is_null() { + FFIError::set_error(error, FFIErrorCode::InvalidInput, "Null pointer provided".to_string()); + return ptr::null_mut(); + } + let account = &*account; + let mnemonic_str = match std::ffi::CStr::from_ptr(mnemonic).to_str() { + Ok(s) => s, + Err(_) => { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Invalid mnemonic string".into(), + ); + return ptr::null_mut(); + } + }; + let passphrase_str = if passphrase.is_null() { + None + } else { + match std::ffi::CStr::from_ptr(passphrase).to_str() { + Ok(s) => Some(s), + Err(_) => { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Invalid passphrase string".into(), + ); + return ptr::null_mut(); + } + } + }; + match account.inner().derive_from_mnemonic_private_key_at( + mnemonic_str, + passphrase_str, + key_wallet::mnemonic::Language::English, + index, + ) { + Ok(sk) => { + let hex = hex::encode(sk.to_bytes()); + match CString::new(hex) { + Ok(s) => { + FFIError::set_success(error); + s.into_raw() + } + Err(_) => { + FFIError::set_error( + error, + FFIErrorCode::AllocationFailed, + "Allocation failed".into(), + ); + ptr::null_mut() + } + } + } + Err(e) => { + FFIError::set_error( + error, + FFIErrorCode::WalletError, + format!("Failed to derive EdDSA private key from mnemonic: {:?}", e), + ); + ptr::null_mut() + } + } +} + +/// Derive a private key (secp256k1) from an account at a given chain/index, using the provided master xpriv. +/// Returns an opaque FFIPrivateKey pointer that must be freed with `private_key_free`. +/// +/// # Safety +/// - `account` and `master_xpriv` must be valid pointers allocated by this library +/// - `error` must be a valid pointer to an FFIError or null +#[no_mangle] +pub unsafe extern "C" fn account_derive_private_key_at( + account: *const FFIAccount, + master_xpriv: *const FFIExtendedPrivateKey, + index: c_uint, + error: *mut FFIError, +) -> *mut FFIPrivateKey { + if account.is_null() || master_xpriv.is_null() { + FFIError::set_error(error, FFIErrorCode::InvalidInput, "Null pointer provided".to_string()); + return ptr::null_mut(); + } + + let account = &*account; + let master_xpriv = &*master_xpriv; + + if account.inner().is_watch_only() { + FFIError::set_error( + error, + FFIErrorCode::WalletError, + "Account is watch-only; private derivation not allowed".to_string(), + ); + return ptr::null_mut(); + } + + match account.inner().derive_from_master_xpriv_extended_xpriv_at(master_xpriv.inner(), index) { + Ok(derived) => { + FFIError::set_success(error); + Box::into_raw(Box::new(FFIPrivateKey::from_secret(derived.private_key))) + } + Err(e) => { + FFIError::set_error( + error, + FFIErrorCode::WalletError, + format!("Failed to derive private key: {:?}", e), + ); + ptr::null_mut() + } + } +} + +/// Derive a private key from an account at a given chain/index and return as WIF string. +/// Caller must free the returned string with `string_free`. +/// +/// # Safety +/// - `account` and `master_xpriv` must be valid pointers allocated by this library +/// - `error` must be a valid pointer to an FFIError or null +#[no_mangle] +pub unsafe extern "C" fn account_derive_private_key_as_wif_at( + account: *const FFIAccount, + master_xpriv: *const FFIExtendedPrivateKey, + index: c_uint, + error: *mut FFIError, +) -> *mut c_char { + if account.is_null() || master_xpriv.is_null() { + FFIError::set_error(error, FFIErrorCode::InvalidInput, "Null pointer provided".to_string()); + return ptr::null_mut(); + } + + let account = &*account; + let master_xpriv = &*master_xpriv; + + if account.inner().is_watch_only() { + FFIError::set_error( + error, + FFIErrorCode::WalletError, + "Account is watch-only; private derivation not allowed".to_string(), + ); + return ptr::null_mut(); + } + + match account.inner().derive_from_master_xpriv_extended_xpriv_at(master_xpriv.inner(), index) { + Ok(derived) => { + // Wrap into dashcore::PrivateKey to WIF encode + let dash_priv = dashcore::PrivateKey { + compressed: true, + network: account.inner().network(), + inner: derived.private_key, + }; + match CString::new(dash_priv.to_wif()) { + Ok(c_str) => { + FFIError::set_success(error); + c_str.into_raw() + } + Err(_) => { + FFIError::set_error( + error, + FFIErrorCode::AllocationFailed, + "Failed to allocate WIF string".to_string(), + ); + ptr::null_mut() + } + } + } + Err(e) => { + FFIError::set_error( + error, + FFIErrorCode::WalletError, + format!("Failed to derive private key: {:?}", e), + ); + ptr::null_mut() + } + } +} + +/// Derive an extended private key from a raw seed buffer at the given index. +/// Returns an opaque FFIExtendedPrivateKey pointer that must be freed with `extended_private_key_free`. +/// +/// # Safety +/// - `account` must be a valid pointer to an FFIAccount +/// - `seed` must point to a valid buffer of length `seed_len` +/// - `error` must be a valid pointer to an FFIError or null +#[no_mangle] +pub unsafe extern "C" fn account_derive_extended_private_key_from_seed( + account: *const FFIAccount, + seed: *const u8, + seed_len: usize, + index: c_uint, + error: *mut FFIError, +) -> *mut FFIExtendedPrivateKey { + if account.is_null() || seed.is_null() { + FFIError::set_error(error, FFIErrorCode::InvalidInput, "Null pointer provided".to_string()); + return ptr::null_mut(); + } + + let account = &*account; + let seed_slice = std::slice::from_raw_parts(seed, seed_len); + + match account.inner().derive_from_seed_extended_xpriv_at(seed_slice, index) { + Ok(derived) => { + FFIError::set_success(error); + Box::into_raw(Box::new(FFIExtendedPrivateKey::from_inner(derived))) + } + Err(e) => { + FFIError::set_error( + error, + FFIErrorCode::WalletError, + format!("Failed to derive extended private key from seed: {:?}", e), + ); + ptr::null_mut() + } + } +} + +/// Derive a private key from a raw seed buffer at the given index. +/// Returns an opaque FFIPrivateKey pointer that must be freed with `private_key_free`. +/// +/// # Safety +/// - `account` must be a valid pointer to an FFIAccount +/// - `seed` must point to a valid buffer of length `seed_len` +/// - `error` must be a valid pointer to an FFIError or null +#[no_mangle] +pub unsafe extern "C" fn account_derive_private_key_from_seed( + account: *const FFIAccount, + seed: *const u8, + seed_len: usize, + index: c_uint, + error: *mut FFIError, +) -> *mut FFIPrivateKey { + if account.is_null() || seed.is_null() { + FFIError::set_error(error, FFIErrorCode::InvalidInput, "Null pointer provided".to_string()); + return ptr::null_mut(); + } + + let account = &*account; + let seed_slice = std::slice::from_raw_parts(seed, seed_len); + + match account.inner().derive_from_seed_extended_xpriv_at(seed_slice, index) { + Ok(derived) => { + FFIError::set_success(error); + Box::into_raw(Box::new(FFIPrivateKey::from_secret(derived.private_key))) + } + Err(e) => { + FFIError::set_error( + error, + FFIErrorCode::WalletError, + format!("Failed to derive private key from seed: {:?}", e), + ); + ptr::null_mut() + } + } +} + +/// Derive an extended private key from a mnemonic + optional passphrase at the given index. +/// Returns an opaque FFIExtendedPrivateKey pointer that must be freed with `extended_private_key_free`. +/// +/// # Safety +/// - `account` must be a valid pointer to an FFIAccount +/// - `mnemonic` must be a valid, null-terminated C string +/// - `passphrase` may be null; if not null, must be a valid C string +/// - `error` must be a valid pointer to an FFIError or null +#[no_mangle] +pub unsafe extern "C" fn account_derive_extended_private_key_from_mnemonic( + account: *const FFIAccount, + mnemonic: *const c_char, + passphrase: *const c_char, + index: c_uint, + error: *mut FFIError, +) -> *mut FFIExtendedPrivateKey { + if account.is_null() || mnemonic.is_null() { + FFIError::set_error(error, FFIErrorCode::InvalidInput, "Null pointer provided".to_string()); + return ptr::null_mut(); + } + + let account = &*account; + let mnemonic_str = match std::ffi::CStr::from_ptr(mnemonic).to_str() { + Ok(s) => s, + Err(_) => { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Invalid mnemonic string".to_string(), + ); + return ptr::null_mut(); + } + }; + let passphrase_str = if passphrase.is_null() { + None + } else { + match std::ffi::CStr::from_ptr(passphrase).to_str() { + Ok(s) => Some(s), + Err(_) => { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Invalid passphrase string".to_string(), + ); + return ptr::null_mut(); + } + } + }; + + match account.inner().derive_from_mnemonic_extended_xpriv_at( + mnemonic_str, + passphrase_str, + key_wallet::mnemonic::Language::English, + index, + ) { + Ok(derived) => { + FFIError::set_success(error); + Box::into_raw(Box::new(FFIExtendedPrivateKey::from_inner(derived))) + } + Err(e) => { + FFIError::set_error( + error, + FFIErrorCode::WalletError, + format!("Failed to derive extended private key from mnemonic: {:?}", e), + ); + ptr::null_mut() + } + } +} + +/// Derive a private key from a mnemonic + optional passphrase at the given index. +/// Returns an opaque FFIPrivateKey pointer that must be freed with `private_key_free`. +/// +/// # Safety +/// - `account` must be a valid pointer to an FFIAccount +/// - `mnemonic` must be a valid, null-terminated C string +/// - `passphrase` may be null; if not null, must be a valid C string +/// - `error` must be a valid pointer to an FFIError or null +#[no_mangle] +pub unsafe extern "C" fn account_derive_private_key_from_mnemonic( + account: *const FFIAccount, + mnemonic: *const c_char, + passphrase: *const c_char, + index: c_uint, + error: *mut FFIError, +) -> *mut FFIPrivateKey { + if account.is_null() || mnemonic.is_null() { + FFIError::set_error(error, FFIErrorCode::InvalidInput, "Null pointer provided".to_string()); + return ptr::null_mut(); + } + + let account = &*account; + let mnemonic_str = match std::ffi::CStr::from_ptr(mnemonic).to_str() { + Ok(s) => s, + Err(_) => { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Invalid mnemonic string".to_string(), + ); + return ptr::null_mut(); + } + }; + let passphrase_str = if passphrase.is_null() { + None + } else { + match std::ffi::CStr::from_ptr(passphrase).to_str() { + Ok(s) => Some(s), + Err(_) => { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Invalid passphrase string".to_string(), + ); + return ptr::null_mut(); + } + } + }; + + match account.inner().derive_from_mnemonic_extended_xpriv_at( + mnemonic_str, + passphrase_str, + key_wallet::mnemonic::Language::English, + index, + ) { + Ok(derived) => { + FFIError::set_success(error); + Box::into_raw(Box::new(FFIPrivateKey::from_secret(derived.private_key))) + } + Err(e) => { + FFIError::set_error( + error, + FFIErrorCode::WalletError, + format!("Failed to derive private key from mnemonic: {:?}", e), + ); + ptr::null_mut() + } + } +} diff --git a/key-wallet-ffi/src/account_derivation_tests.rs b/key-wallet-ffi/src/account_derivation_tests.rs new file mode 100644 index 000000000..920a331df --- /dev/null +++ b/key-wallet-ffi/src/account_derivation_tests.rs @@ -0,0 +1,221 @@ +//! Tests for account-level derivation FFI + +#[cfg(test)] +mod tests { + use crate::account::account_free; + use crate::account_derivation::*; + use crate::derivation::*; + use crate::error::{FFIError, FFIErrorCode}; + use crate::keys::{extended_private_key_free, private_key_free}; + use crate::types::{FFIAccountType, FFINetworks}; + use crate::wallet; + + const MNEMONIC: &str = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + + #[test] + fn test_account_derive_private_key_at_receive_index() { + let mut error = FFIError::success(); + + // Create wallet on testnet with default accounts + let wallet = unsafe { wallet::wallet_create_from_mnemonic(c_str(MNEMONIC), c_str(""), FFINetworks::TestnetFlag, &mut error) }; + assert!(!wallet.is_null()); + assert_eq!(unsafe { (*(&mut error)).code }, FFIErrorCode::Success); + + // Get account 0 (BIP44) + let account = unsafe { + crate::account::wallet_get_account(wallet, crate::FFINetwork::Testnet, 0, FFIAccountType::StandardBIP44) + .account + }; + assert!(!account.is_null()); + + // Build a master xpriv from the same mnemonic seed + let mut seed = [0u8; 64]; + // Deterministic seed from mnemonic helper + let ok = unsafe { + crate::mnemonic::mnemonic_to_seed(c_str(MNEMONIC), c_str(""), seed.as_mut_ptr(), &mut (seed.len()), &mut error) + }; + assert!(ok); + + let master_xpriv = unsafe { derivation_new_master_key(seed.as_ptr(), seed.len(), crate::FFINetwork::Testnet, &mut error) }; + assert!(!master_xpriv.is_null()); + + // For standard accounts with internal/external, this helper should fail + let priv_key = unsafe { account_derive_private_key_at(account, master_xpriv, 0, &mut error) }; + assert!(priv_key.is_null()); + assert_eq!(unsafe { (*(&mut error)).code }, FFIErrorCode::WalletError); + + // Derive WIF should also fail for such accounts + let wif = unsafe { account_derive_private_key_as_wif_at(account, master_xpriv, 0, &mut error) }; + assert!(wif.is_null()); + assert_eq!(unsafe { (*(&mut error)).code }, FFIErrorCode::WalletError); + + // Cleanup + unsafe { + crate::utils::string_free(wif); + private_key_free(priv_key); + extended_private_key_free(master_xpriv); + account_free(account); + wallet::wallet_free(wallet); + } + } + + #[test] + fn test_bls_and_eddsa_from_seed_and_mnemonic_null_safety() { + let mut error = FFIError::success(); + + // BLS nulls + #[cfg(feature = "bls")] + unsafe { + assert!( + super::super::account_derivation::bls_account_derive_private_key_from_seed( + std::ptr::null(), + std::ptr::null(), + 0, + 0, + &mut error, + ) + .is_null() + ); + assert_eq!(error.code, FFIErrorCode::InvalidInput); + } + + // EdDSA nulls + #[cfg(feature = "eddsa")] + unsafe { + assert!( + super::super::account_derivation::eddsa_account_derive_private_key_from_seed( + std::ptr::null(), + std::ptr::null(), + 0, + 0, + &mut error, + ) + .is_null() + ); + assert_eq!(error.code, FFIErrorCode::InvalidInput); + } + } + + #[test] + fn test_account_derive_extended_private_key_at_change_index() { + let mut error = FFIError::success(); + + // Create wallet on testnet with default accounts + let wallet = unsafe { wallet::wallet_create_from_mnemonic(c_str(MNEMONIC), c_str(""), FFINetworks::TestnetFlag, &mut error) }; + assert!(!wallet.is_null()); + + // Get account 0 (BIP44) + let account = unsafe { + crate::account::wallet_get_account(wallet, crate::FFINetwork::Testnet, 0, FFIAccountType::StandardBIP44) + .account + }; + assert!(!account.is_null()); + + // Seed and master xpriv + let mut seed = [0u8; 64]; + let ok = unsafe { + crate::mnemonic::mnemonic_to_seed(c_str(MNEMONIC), c_str(""), seed.as_mut_ptr(), &mut (seed.len()), &mut error) + }; + assert!(ok); + let master_xpriv = unsafe { derivation_new_master_key(seed.as_ptr(), seed.len(), crate::FFINetwork::Testnet, &mut error) }; + assert!(!master_xpriv.is_null()); + + // Extended xpriv helper should also fail for standard accounts + let xpriv = + unsafe { account_derive_extended_private_key_at(account, master_xpriv, 5, &mut error) }; + assert!(xpriv.is_null()); + assert_eq!(unsafe { (*(&mut error)).code }, FFIErrorCode::WalletError); + + // Cleanup + unsafe { + extended_private_key_free(master_xpriv); + account_free(account); + wallet::wallet_free(wallet); + } + } + + #[test] + fn test_account_derive_from_seed_and_mnemonic_helpers_fail_for_standard() { + let mut error = FFIError::success(); + + // Create wallet and get account 0 + let wallet = unsafe { wallet::wallet_create_from_mnemonic(c_str(MNEMONIC), c_str(""), FFINetworks::TestnetFlag, &mut error) }; + assert!(!wallet.is_null()); + let account = unsafe { + crate::account::wallet_get_account(wallet, crate::FFINetwork::Testnet, 0, FFIAccountType::StandardBIP44) + .account + }; + assert!(!account.is_null()); + + // Prepare seed + let mnemonic = std::ffi::CString::new(MNEMONIC).unwrap(); + let pass = std::ffi::CString::new("").unwrap(); + let mut seed = [0u8; 64]; + let mut seed_len = seed.len(); + let ok = unsafe { crate::mnemonic::mnemonic_to_seed(mnemonic.as_ptr(), pass.as_ptr(), seed.as_mut_ptr(), &mut seed_len, &mut error) }; + assert!(ok); + + // account_derive_extended_private_key_from_seed should fail for standard accounts + let xpriv_seed = unsafe { + super::super::account_derivation::account_derive_extended_private_key_from_seed( + account, + seed.as_ptr(), + seed_len, + 0, + &mut error, + ) + }; + assert!(xpriv_seed.is_null()); + assert_eq!(unsafe { (*(&mut error)).code }, FFIErrorCode::WalletError); + + // account_derive_private_key_from_seed should fail + let priv_seed = unsafe { + super::super::account_derivation::account_derive_private_key_from_seed( + account, + seed.as_ptr(), + seed_len, + 0, + &mut error, + ) + }; + assert!(priv_seed.is_null()); + assert_eq!(unsafe { (*(&mut error)).code }, FFIErrorCode::WalletError); + + // account_derive_extended_private_key_from_mnemonic should fail + let xpriv_mn = unsafe { + super::super::account_derivation::account_derive_extended_private_key_from_mnemonic( + account, + mnemonic.as_ptr(), + pass.as_ptr(), + 0, + &mut error, + ) + }; + assert!(xpriv_mn.is_null()); + assert_eq!(unsafe { (*(&mut error)).code }, FFIErrorCode::WalletError); + + // account_derive_private_key_from_mnemonic should fail + let priv_mn = unsafe { + super::super::account_derivation::account_derive_private_key_from_mnemonic( + account, + mnemonic.as_ptr(), + pass.as_ptr(), + 0, + &mut error, + ) + }; + assert!(priv_mn.is_null()); + assert_eq!(unsafe { (*(&mut error)).code }, FFIErrorCode::WalletError); + + unsafe { + account_free(account); + wallet::wallet_free(wallet); + } + } + + // Helper to make C string pointers + fn c_str(s: &str) -> *const i8 { + std::ffi::CString::new(s).unwrap().as_ptr() + } +} diff --git a/key-wallet-ffi/src/derivation.rs b/key-wallet-ffi/src/derivation.rs index d2ceb5c4c..b96de79d3 100644 --- a/key-wallet-ffi/src/derivation.rs +++ b/key-wallet-ffi/src/derivation.rs @@ -699,171 +699,6 @@ pub unsafe extern "C" fn derivation_string_free(s: *mut c_char) { } } -/// Derive key using DIP9 path constants for identity -/// -/// # Safety -/// -/// - `seed` must be a valid pointer to a byte array of `seed_len` length -/// - `error` must be a valid pointer to an FFIError structure or null -/// - The caller must ensure the seed pointer remains valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn dip9_derive_identity_key( - seed: *const u8, - seed_len: usize, - network: FFINetwork, - identity_index: c_uint, - key_index: c_uint, - key_type: FFIDerivationPathType, - error: *mut FFIError, -) -> *mut FFIExtendedPrivKey { - if seed.is_null() { - FFIError::set_error(error, FFIErrorCode::InvalidInput, "Seed is null".to_string()); - return ptr::null_mut(); - } - - let seed_slice = slice::from_raw_parts(seed, seed_len); - let network_rust: Network = network.into(); - - use key_wallet::bip32::{ChildNumber, DerivationPath}; - use key_wallet::dip9::{ - IDENTITY_AUTHENTICATION_PATH_MAINNET, IDENTITY_AUTHENTICATION_PATH_TESTNET, - IDENTITY_REGISTRATION_PATH_MAINNET, IDENTITY_REGISTRATION_PATH_TESTNET, - IDENTITY_TOPUP_PATH_MAINNET, IDENTITY_TOPUP_PATH_TESTNET, - }; - - let base_path = match (network_rust, key_type) { - (key_wallet::Network::Dash, FFIDerivationPathType::PathBlockchainIdentities) => { - IDENTITY_AUTHENTICATION_PATH_MAINNET - } - ( - key_wallet::Network::Testnet - | key_wallet::Network::Devnet - | key_wallet::Network::Regtest, - FFIDerivationPathType::PathBlockchainIdentities, - ) => IDENTITY_AUTHENTICATION_PATH_TESTNET, - ( - key_wallet::Network::Dash, - FFIDerivationPathType::PathBlockchainIdentityCreditRegistrationFunding, - ) => IDENTITY_REGISTRATION_PATH_MAINNET, - ( - key_wallet::Network::Testnet - | key_wallet::Network::Devnet - | key_wallet::Network::Regtest, - FFIDerivationPathType::PathBlockchainIdentityCreditRegistrationFunding, - ) => IDENTITY_REGISTRATION_PATH_TESTNET, - ( - key_wallet::Network::Dash, - FFIDerivationPathType::PathBlockchainIdentityCreditTopupFunding, - ) => IDENTITY_TOPUP_PATH_MAINNET, - ( - key_wallet::Network::Testnet - | key_wallet::Network::Devnet - | key_wallet::Network::Regtest, - FFIDerivationPathType::PathBlockchainIdentityCreditTopupFunding, - ) => IDENTITY_TOPUP_PATH_TESTNET, - _ => { - FFIError::set_error( - error, - FFIErrorCode::InvalidInput, - "Invalid key type for identity derivation".to_string(), - ); - return ptr::null_mut(); - } - }; - - // Build additional path based on key type - let additional_path = match key_type { - FFIDerivationPathType::PathBlockchainIdentities => { - // Authentication: identity_index'/key_index' - let cn1 = match ChildNumber::from_hardened_idx(identity_index) { - Ok(v) => v, - Err(e) => { - FFIError::set_error( - error, - FFIErrorCode::InvalidDerivationPath, - format!("Invalid identity_index: {}", e), - ); - return ptr::null_mut(); - } - }; - let cn2 = match ChildNumber::from_hardened_idx(key_index) { - Ok(v) => v, - Err(e) => { - FFIError::set_error( - error, - FFIErrorCode::InvalidDerivationPath, - format!("Invalid key_index: {}", e), - ); - return ptr::null_mut(); - } - }; - DerivationPath::from(vec![cn1, cn2]) - } - FFIDerivationPathType::PathBlockchainIdentityCreditRegistrationFunding => { - // Registration: index' - let cn = match ChildNumber::from_hardened_idx(identity_index) { - Ok(v) => v, - Err(e) => { - FFIError::set_error( - error, - FFIErrorCode::InvalidDerivationPath, - format!("Invalid identity_index: {}", e), - ); - return ptr::null_mut(); - } - }; - DerivationPath::from(vec![cn]) - } - FFIDerivationPathType::PathBlockchainIdentityCreditTopupFunding => { - // Top-up: identity_index'/topup_index' - let cn1 = match ChildNumber::from_hardened_idx(identity_index) { - Ok(v) => v, - Err(e) => { - FFIError::set_error( - error, - FFIErrorCode::InvalidDerivationPath, - format!("Invalid identity_index: {}", e), - ); - return ptr::null_mut(); - } - }; - let cn2 = match ChildNumber::from_hardened_idx(key_index) { - Ok(v) => v, - Err(e) => { - FFIError::set_error( - error, - FFIErrorCode::InvalidDerivationPath, - format!("Invalid topup_index: {}", e), - ); - return ptr::null_mut(); - } - }; - DerivationPath::from(vec![cn1, cn2]) - } - _ => { - FFIError::set_error(error, FFIErrorCode::InvalidInput, "Invalid key type".to_string()); - return ptr::null_mut(); - } - }; - - match base_path.derive_priv_ecdsa_for_master_seed(seed_slice, additional_path, network_rust) { - Ok(xpriv) => { - FFIError::set_success(error); - Box::into_raw(Box::new(FFIExtendedPrivKey { - inner: xpriv, - })) - } - Err(e) => { - FFIError::set_error( - error, - FFIErrorCode::WalletError, - format!("Failed to derive identity key: {:?}", e), - ); - ptr::null_mut() - } - } -} - // MARK: - Simplified Derivation Functions /// Derive an address from a private key diff --git a/key-wallet-ffi/src/derivation_tests.rs b/key-wallet-ffi/src/derivation_tests.rs index ce4b64ed4..d56349584 100644 --- a/key-wallet-ffi/src/derivation_tests.rs +++ b/key-wallet-ffi/src/derivation_tests.rs @@ -291,37 +291,6 @@ mod tests { } } - #[test] - fn test_dip9_derive_identity_key() { - let mut error = FFIError::success(); - - // Generate a seed - let mut seed = [0u8; 64]; - for (i, byte) in seed.iter_mut().enumerate() { - *byte = (i % 256) as u8; - } - - // Derive identity key - takes seed directly, not xprv - let identity_key = unsafe { - dip9_derive_identity_key( - seed.as_ptr(), - seed.len(), - FFINetwork::Testnet, - 0, // identity index - 0, // key index - FFIDerivationPathType::PathBlockchainIdentities, // key_type - &mut error, - ) - }; - - assert!(!identity_key.is_null()); - - // Clean up - unsafe { - derivation_xpriv_free(identity_key); - } - } - #[test] fn test_error_handling() { let mut error = FFIError::success(); @@ -758,54 +727,6 @@ mod tests { } } - #[test] - fn test_dip9_derive_identity_key_null_inputs() { - let mut error = FFIError::success(); - - // Test with null seed - let identity_key = unsafe { - dip9_derive_identity_key( - ptr::null(), - 64, - FFINetwork::Testnet, - 0, - 0, - FFIDerivationPathType::PathBlockchainIdentities, - &mut error, - ) - }; - assert!(identity_key.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_dip9_derive_identity_key_different_types() { - let mut error = FFIError::success(); - let mut seed = [0u8; 64]; - for (i, byte) in seed.iter_mut().enumerate() { - *byte = (i % 256) as u8; - } - - // Test the main derivation path type that we know works - let identity_key = unsafe { - dip9_derive_identity_key( - seed.as_ptr(), - seed.len(), - FFINetwork::Testnet, - 0, - 0, - FFIDerivationPathType::PathBlockchainIdentities, - &mut error, - ) - }; - - if !identity_key.is_null() { - unsafe { - derivation_xpriv_free(identity_key); - } - } - } - #[test] fn test_identity_path_functions_null_inputs() { let mut error = FFIError::success(); diff --git a/key-wallet-ffi/src/keys.rs b/key-wallet-ffi/src/keys.rs index 5efe97a3e..a3129829d 100644 --- a/key-wallet-ffi/src/keys.rs +++ b/key-wallet-ffi/src/keys.rs @@ -26,6 +26,29 @@ pub struct FFIExtendedPublicKey { inner: key_wallet::bip32::ExtendedPubKey, } +impl FFIExtendedPrivateKey { + #[inline] + pub(crate) fn inner(&self) -> &key_wallet::bip32::ExtendedPrivKey { + &self.inner + } + + #[inline] + pub(crate) fn from_inner(inner: key_wallet::bip32::ExtendedPrivKey) -> Self { + FFIExtendedPrivateKey { + inner, + } + } +} + +impl FFIPrivateKey { + #[inline] + pub(crate) fn from_secret(inner: secp256k1::SecretKey) -> Self { + FFIPrivateKey { + inner, + } + } +} + /// Get extended private key for account /// /// # Safety diff --git a/key-wallet-ffi/src/lib.rs b/key-wallet-ffi/src/lib.rs index 6338155e9..7fbb41b46 100644 --- a/key-wallet-ffi/src/lib.rs +++ b/key-wallet-ffi/src/lib.rs @@ -6,6 +6,7 @@ // Module declarations pub mod account; pub mod account_collection; +pub mod account_derivation; pub mod address; pub mod address_pool; pub mod derivation; diff --git a/key-wallet/src/account/bls_account.rs b/key-wallet/src/account/bls_account.rs index 216d2b9b4..94c4d188b 100644 --- a/key-wallet/src/account/bls_account.rs +++ b/key-wallet/src/account/bls_account.rs @@ -78,7 +78,7 @@ impl BLSAccount { network, depth: 0, parent_fingerprint: Fingerprint::default(), - child_number: ChildNumber::from_normal_idx(0).unwrap(), + child_number: ChildNumber::from_normal_idx(0).expect("Invalid child number"), public_key, chain_code: ChainCode::from([0u8; 32]), }; @@ -225,9 +225,33 @@ impl fmt::Display for BLSAccount { } } -impl AccountDerivation> - for BLSAccount +impl + AccountDerivation< + ExtendedBLSPrivKey, + ExtendedBLSPubKey, + BLSPublicKey, + SecretKey, + > for BLSAccount { + fn defaults_to_hardened_derivation(&self) -> bool { + false + } + + fn has_internal_and_external(&self) -> bool { + true + } + + fn has_intermediate_derivation(&self) -> Option { + match self.account_type { + AccountType::IdentityTopUp { + registration_index, + } => Some(ChildNumber::Hardened { + index: registration_index, + }), + _ => None, + } + } + /// Derive an extended private key from the wallet's master BLS private key /// using the BLS account's derivation path. /// @@ -363,6 +387,34 @@ impl AccountDerivation Result> { + let xpriv = self.derive_from_master_xpriv_extended_xpriv_at(master_xpriv, index)?; + Ok(xpriv.private_key.clone()) + } + + fn derive_from_seed_extended_xpriv_at( + &self, + seed: &[u8], + index: u32, + ) -> Result { + let master = ExtendedBLSPrivKey::new_master(self.network, seed) + .map_err(|e| Error::InvalidParameter(format!("BLS master from seed: {:?}", e)))?; + self.derive_from_master_xpriv_extended_xpriv_at(&master, index) + } + + fn derive_from_seed_private_key_at( + &self, + seed: &[u8], + index: u32, + ) -> Result> { + let xpriv = self.derive_from_seed_extended_xpriv_at(seed, index)?; + Ok(xpriv.private_key.clone()) + } } #[cfg(test)] diff --git a/key-wallet/src/account/derivation.rs b/key-wallet/src/account/derivation.rs index e097fe9ef..45a32e45a 100644 --- a/key-wallet/src/account/derivation.rs +++ b/key-wallet/src/account/derivation.rs @@ -1,5 +1,6 @@ use crate::managed_account::address_pool::AddressPoolType; -use crate::{ChildNumber, DerivationPath, Error}; +use crate::mnemonic::Language; +use crate::{ChildNumber, DerivationPath, Error, Mnemonic}; use dashcore::Address; /// Derivation helpers available on an account-like type. @@ -9,7 +10,24 @@ use dashcore::Address; /// - Hardened indices are in `[0, 2^31 - 1]` and marked `'` conceptually. /// - Implementors may use private state (e.g., `is_watch_only`, `account_xpub`, `network`) /// inside their concrete `impl` blocks; this trait only fixes the public API. -pub trait AccountDerivation { +pub trait AccountDerivation { + /// Whether this account's index derivations default to hardened. + /// + /// For example, Ed25519 (SLIP-0010) requires hardened-only derivation. + fn defaults_to_hardened_derivation(&self) -> bool; + + /// Whether this account uses separate internal/external chains. + /// + /// If true, the simplified helpers below are not applicable and will return an error, + /// since callers must specify which chain to use. + fn has_internal_and_external(&self) -> bool { + false + } + + fn has_intermediate_derivation(&self) -> Option { + None + } + /// Derive an extended private key from the wallet’s master xpriv /// using the implementor’s account derivation path. /// @@ -117,4 +135,92 @@ pub trait AccountDerivation { index: u32, use_hardened_with_priv_key: Option, ) -> Result; + + /// Derive an extended private key at the given chain and index + /// starting from the wallet's master extended private key. + /// + /// Default implementation derives the account xpriv from master, then + /// appends the (chain, index) tail. External/Internal use non-hardened + /// indices; AbsentHardened uses hardened index. + fn derive_from_master_xpriv_extended_xpriv_at( + &self, + master_xpriv: &EPrivKeyType, + index: u32, + ) -> Result + where + Self: Sized, + { + // Disallow when account has both internal and external chains + if self.has_internal_and_external() { + return Err(Error::InvalidParameter( + "Account has internal/external chains; chain-agnostic derivation not applicable" + .into(), + )); + } + + // Derive account-level xpriv first + let account_xpriv = self.derive_xpriv_from_master_xpriv(master_xpriv)?; + // Build the child derivation path relative to the account + let child_path = if let Some(intermediate) = self.has_intermediate_derivation() { + DerivationPath::from(vec![ + intermediate, + ChildNumber::from_idx(index, self.defaults_to_hardened_derivation())?, + ]) + } else { + DerivationPath::from(vec![ChildNumber::from_idx( + index, + self.defaults_to_hardened_derivation(), + )?]) + }; + // Derive the child extended private key + self.derive_child_xpriv_from_account_xpriv(&account_xpriv, &child_path) + } + + /// Derive a raw private key at the given chain and index + /// starting from the wallet's master extended private key. + fn derive_from_master_xpriv_private_key_at( + &self, + master_xpriv: &EPrivKeyType, + index: u32, + ) -> Result; + + /// Derive an extended private key from a raw seed at the given index. + fn derive_from_seed_extended_xpriv_at( + &self, + seed: &[u8], + index: u32, + ) -> Result; + + /// Derive a private key from a raw seed at the given index. + fn derive_from_seed_private_key_at( + &self, + seed: &[u8], + index: u32, + ) -> Result; + + /// Derive an extended private key from a BIP39 mnemonic and optional passphrase at the given index. + fn derive_from_mnemonic_extended_xpriv_at( + &self, + mnemonic: &str, + passphrase: Option<&str>, + language: Language, + index: u32, + ) -> Result { + let m = Mnemonic::from_phrase(mnemonic, language)?; + let seed = m.to_seed(passphrase.unwrap_or("")); + self.derive_from_seed_extended_xpriv_at(&seed, index) + } + + /// Derive a private key from a BIP39 mnemonic and optional passphrase at the given index. + fn derive_from_mnemonic_private_key_at( + &self, + mnemonic: &str, + passphrase: Option<&str>, + language: Language, + index: u32, + ) -> Result { + let m = Mnemonic::from_phrase(mnemonic, language)?; + let seed = m.to_seed(passphrase.unwrap_or("")); + self.derive_from_seed_private_key_at(&seed, index) + } } diff --git a/key-wallet/src/account/eddsa_account.rs b/key-wallet/src/account/eddsa_account.rs index a92d3d2ab..efbf40ff4 100644 --- a/key-wallet/src/account/eddsa_account.rs +++ b/key-wallet/src/account/eddsa_account.rs @@ -226,9 +226,32 @@ impl fmt::Display for EdDSAAccount { } } -impl AccountDerivation - for EdDSAAccount +impl + AccountDerivation< + ExtendedEd25519PrivKey, + ExtendedEd25519PubKey, + VerifyingKey, + dashcore::ed25519_dalek::SigningKey, + > for EdDSAAccount { + fn defaults_to_hardened_derivation(&self) -> bool { + true + } + + fn has_internal_and_external(&self) -> bool { + false + } + + fn has_intermediate_derivation(&self) -> Option { + match self.account_type { + AccountType::IdentityTopUp { + registration_index, + } => Some(ChildNumber::Hardened { + index: registration_index, + }), + _ => None, + } + } /// Derive an extended private key from the wallet's master Ed25519 private key /// using the EdDSA account's derivation path. /// @@ -361,6 +384,33 @@ impl AccountDerivation Result { + let xpriv = self.derive_from_master_xpriv_extended_xpriv_at(master_xpriv, index)?; + Ok(dashcore::ed25519_dalek::SigningKey::from_bytes(&xpriv.private_key)) + } + + fn derive_from_seed_extended_xpriv_at( + &self, + seed: &[u8], + index: u32, + ) -> Result { + let master = ExtendedEd25519PrivKey::new_master(self.network, seed)?; + self.derive_from_master_xpriv_extended_xpriv_at(&master, index) + } + + fn derive_from_seed_private_key_at( + &self, + seed: &[u8], + index: u32, + ) -> Result { + let xpriv = self.derive_from_seed_extended_xpriv_at(seed, index)?; + Ok(dashcore::ed25519_dalek::SigningKey::from_bytes(&xpriv.private_key)) + } } #[cfg(test)] diff --git a/key-wallet/src/account/mod.rs b/key-wallet/src/account/mod.rs index d15266985..4b56c80ca 100644 --- a/key-wallet/src/account/mod.rs +++ b/key-wallet/src/account/mod.rs @@ -27,7 +27,7 @@ use serde::{Deserialize, Serialize}; use crate::bip32::{DerivationPath, ExtendedPrivKey, ExtendedPubKey}; use crate::dip9::DerivationPathReference; use crate::error::Result; -use crate::{Error, Network}; +use crate::{ChildNumber, Error, Network}; use crate::account::derivation::AccountDerivation; use crate::managed_account::address_pool::AddressPoolType; @@ -163,7 +163,27 @@ impl AccountTrait for Account { } } -impl AccountDerivation for Account { +impl AccountDerivation + for Account +{ + fn defaults_to_hardened_derivation(&self) -> bool { + false + } + + fn has_intermediate_derivation(&self) -> Option { + match self.account_type { + AccountType::IdentityTopUp { + registration_index, + } => Some(ChildNumber::Hardened { + index: registration_index, + }), + _ => None, + } + } + + fn has_internal_and_external(&self) -> bool { + matches!(self.account_type, AccountType::Standard { .. }) + } /// Derive an extended private key from a wallet's master private key /// /// This requires the wallet to have the master private key available. @@ -293,10 +313,46 @@ impl AccountDerivation for Account { self.derive_child_xpub(&derivation_path) } } + + fn derive_from_master_xpriv_private_key_at( + &self, + master_xpriv: &ExtendedPrivKey, + index: u32, + ) -> std::result::Result { + let xpriv = self.derive_from_master_xpriv_extended_xpriv_at(master_xpriv, index)?; + // Wrap into dashcore::PrivateKey with compressed=true + Ok(dashcore::PrivateKey { + compressed: true, + network: self.network, + inner: xpriv.private_key, + }) + } + + fn derive_from_seed_extended_xpriv_at( + &self, + seed: &[u8], + index: u32, + ) -> std::result::Result { + let master = ExtendedPrivKey::new_master(self.network, seed).map_err(Error::Bip32)?; + self.derive_from_master_xpriv_extended_xpriv_at(&master, index) + } + + fn derive_from_seed_private_key_at( + &self, + seed: &[u8], + index: u32, + ) -> std::result::Result { + let xpriv = self.derive_from_seed_extended_xpriv_at(seed, index)?; + Ok(dashcore::PrivateKey { + compressed: true, + network: self.network, + inner: xpriv.private_key, + }) + } } pub trait ECDSAAddressDerivation: - AccountDerivation + AccountDerivation { /// Derive a receive (external) address at a specific index fn derive_receive_address(&self, index: u32) -> Result
{