minor refactor. update ffi

This commit is contained in:
Sergio Chouhy 2026-04-17 19:45:30 -03:00
parent b34e301023
commit a42144cb3c
7 changed files with 127 additions and 140 deletions

View File

@ -48,11 +48,13 @@ unsafe extern "C" {
out_account_id: *mut FfiBytes32, out_account_id: *mut FfiBytes32,
) -> error::WalletFfiError; ) -> error::WalletFfiError;
fn wallet_ffi_create_account_private( fn wallet_ffi_create_private_accounts_key(
handle: *mut WalletHandle, handle: *mut WalletHandle,
out_account_id: *mut FfiBytes32, out_keys: *mut FfiPrivateAccountKeys,
) -> error::WalletFfiError; ) -> error::WalletFfiError;
fn wallet_ffi_free_private_account_keys(keys: *mut FfiPrivateAccountKeys);
fn wallet_ffi_list_accounts( fn wallet_ffi_list_accounts(
handle: *mut WalletHandle, handle: *mut WalletHandle,
out_list: *mut FfiAccountList, out_list: *mut FfiAccountList,
@ -262,33 +264,32 @@ fn wallet_ffi_create_public_accounts() -> Result<()> {
fn wallet_ffi_create_private_accounts() -> Result<()> { fn wallet_ffi_create_private_accounts() -> Result<()> {
let password = "password_for_tests"; let password = "password_for_tests";
let n_accounts = 10; let n_accounts = 10;
// Create `n_accounts` private accounts with wallet FFI // Create `n_accounts` receiving keys with wallet FFI
let new_private_account_ids_ffi = unsafe { let new_npks_ffi = unsafe {
let mut account_ids = Vec::new(); let mut npks = Vec::new();
let wallet_ffi_handle = new_wallet_ffi_with_default_config(password)?; let wallet_ffi_handle = new_wallet_ffi_with_default_config(password)?;
for _ in 0..n_accounts { for _ in 0..n_accounts {
let mut out_account_id = FfiBytes32::from_bytes([0; 32]); let mut out_keys = FfiPrivateAccountKeys::default();
wallet_ffi_create_account_private(wallet_ffi_handle, &raw mut out_account_id); wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys);
account_ids.push(out_account_id.data); npks.push(out_keys.nullifier_public_key.data);
wallet_ffi_free_private_account_keys(&raw mut out_keys);
} }
wallet_ffi_destroy(wallet_ffi_handle); wallet_ffi_destroy(wallet_ffi_handle);
account_ids npks
}; };
// All returned IDs must be unique and non-zero // All returned NPKs must be unique and non-zero
assert_eq!(new_private_account_ids_ffi.len(), n_accounts); assert_eq!(new_npks_ffi.len(), n_accounts);
let unique: HashSet<_> = new_private_account_ids_ffi.iter().collect(); let unique: HashSet<_> = new_npks_ffi.iter().collect();
assert_eq!( assert_eq!(
unique.len(), unique.len(),
n_accounts, n_accounts,
"Duplicate private account IDs returned" "Duplicate NPKs returned"
); );
assert!( assert!(
new_private_account_ids_ffi new_npks_ffi.iter().all(|id| *id != [0_u8; 32]),
.iter() "Zero NPK returned"
.all(|id| *id != [0_u8; 32]),
"Zero account ID returned"
); );
Ok(()) Ok(())
@ -296,47 +297,34 @@ fn wallet_ffi_create_private_accounts() -> Result<()> {
#[test] #[test]
fn wallet_ffi_save_and_load_persistent_storage() -> Result<()> { fn wallet_ffi_save_and_load_persistent_storage() -> Result<()> {
let ctx = BlockingTestContext::new()?; let ctx = BlockingTestContext::new()?;
let mut out_private_account_id = FfiBytes32::from_bytes([0; 32]);
let home = tempfile::tempdir()?; let home = tempfile::tempdir()?;
let mut first_npk = [0u8; 32];
// Create a private account with the wallet FFI and save it // Create a receiving key and save
unsafe { unsafe {
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
wallet_ffi_create_account_private(wallet_ffi_handle, &raw mut out_private_account_id); let mut out_keys = FfiPrivateAccountKeys::default();
wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys);
first_npk = out_keys.nullifier_public_key.data;
wallet_ffi_free_private_account_keys(&raw mut out_keys);
wallet_ffi_save(wallet_ffi_handle); wallet_ffi_save(wallet_ffi_handle);
wallet_ffi_destroy(wallet_ffi_handle); wallet_ffi_destroy(wallet_ffi_handle);
} }
let private_account_keys = unsafe { // After loading, creating a new key should yield a different NPK (state was persisted)
let second_npk = unsafe {
let wallet_ffi_handle = load_existing_ffi_wallet(home.path())?; let wallet_ffi_handle = load_existing_ffi_wallet(home.path())?;
let mut private_account = FfiAccount::default();
let result = wallet_ffi_get_account_private(
wallet_ffi_handle,
&raw const out_private_account_id,
&raw mut private_account,
);
assert_eq!(result, error::WalletFfiError::Success);
let mut out_keys = FfiPrivateAccountKeys::default(); let mut out_keys = FfiPrivateAccountKeys::default();
let result = wallet_ffi_get_private_account_keys( wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys);
wallet_ffi_handle, let npk = out_keys.nullifier_public_key.data;
&raw const out_private_account_id, wallet_ffi_free_private_account_keys(&raw mut out_keys);
&raw mut out_keys,
);
assert_eq!(result, error::WalletFfiError::Success);
wallet_ffi_destroy(wallet_ffi_handle); wallet_ffi_destroy(wallet_ffi_handle);
npk
out_keys
}; };
assert_eq!( assert_ne!(first_npk, [0u8; 32], "First NPK should be non-zero");
nssa::AccountId::from((&private_account_keys.npk(), 0)), assert_ne!(second_npk, [0u8; 32], "Second NPK should be non-zero");
out_private_account_id.into() assert_ne!(first_npk, second_npk, "Keys should differ after state was persisted");
);
Ok(()) Ok(())
} }
@ -351,15 +339,17 @@ fn test_wallet_ffi_list_accounts() -> Result<()> {
let mut public_ids: Vec<[u8; 32]> = Vec::new(); let mut public_ids: Vec<[u8; 32]> = Vec::new();
let mut private_ids: Vec<[u8; 32]> = Vec::new(); let mut private_ids: Vec<[u8; 32]> = Vec::new();
// Create 5 public accounts and 5 private accounts, recording their IDs // Create 5 public accounts and 5 receiving keys
for _ in 0..5 { for _ in 0..5 {
let mut out_account_id = FfiBytes32::from_bytes([0; 32]); let mut out_account_id = FfiBytes32::from_bytes([0; 32]);
wallet_ffi_create_account_public(handle, &raw mut out_account_id); wallet_ffi_create_account_public(handle, &raw mut out_account_id);
public_ids.push(out_account_id.data); public_ids.push(out_account_id.data);
wallet_ffi_create_account_private(handle, &raw mut out_account_id); let mut out_keys = FfiPrivateAccountKeys::default();
private_ids.push(out_account_id.data); wallet_ffi_create_private_accounts_key(handle, &raw mut out_keys);
wallet_ffi_free_private_account_keys(&raw mut out_keys);
} }
let _ = private_ids;
(handle, public_ids, private_ids) (handle, public_ids, private_ids)
}; };
@ -396,19 +386,13 @@ fn test_wallet_ffi_list_accounts() -> Result<()> {
"Created public account not found in list with is_public=true" "Created public account not found in list with is_public=true"
); );
} }
for id in &created_private_ids { // Total listed accounts must be at least the number of public accounts created
assert!( // (receiving keys without synced accounts don't appear in the list)
listed_private_ids.contains(id),
"Created private account not found in list with is_public=false"
);
}
// Total listed accounts must be at least the number we created
assert!( assert!(
wallet_ffi_account_list.count >= created_public_ids.len() + created_private_ids.len(), wallet_ffi_account_list.count >= created_public_ids.len(),
"Listed account count ({}) is less than the number of created accounts ({})", "Listed account count ({}) is less than the number of created public accounts ({})",
wallet_ffi_account_list.count, wallet_ffi_account_list.count,
created_public_ids.len() + created_private_ids.len() created_public_ids.len()
); );
unsafe { unsafe {
@ -712,25 +696,16 @@ fn wallet_ffi_init_private_account_auth_transfer() -> Result<()> {
let home = tempfile::tempdir()?; let home = tempfile::tempdir()?;
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
// Create a new uninitialized public account // Create a new receiving key and derive account_id for identifier=0
let mut out_account_id = FfiBytes32::from_bytes([0; 32]); let out_account_id: FfiBytes32 = unsafe {
unsafe { let mut out_keys = FfiPrivateAccountKeys::default();
wallet_ffi_create_account_private(wallet_ffi_handle, &raw mut out_account_id); wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys);
} let account_id = nssa::AccountId::from((&out_keys.npk(), 0_u128));
wallet_ffi_free_private_account_keys(&raw mut out_keys);
// Check its program owner is the default program id (&account_id).into()
let account: Account = unsafe {
let mut out_account = FfiAccount::default();
wallet_ffi_get_account_private(
wallet_ffi_handle,
&raw const out_account_id,
&raw mut out_account,
);
(&out_account).try_into().unwrap()
}; };
assert_eq!(account.program_owner, DEFAULT_PROGRAM_ID);
// Call the init funciton // Call the init function
let mut transfer_result = FfiTransferResult::default(); let mut transfer_result = FfiTransferResult::default();
unsafe { unsafe {
wallet_ffi_register_private_account( wallet_ffi_register_private_account(
@ -834,15 +809,11 @@ fn test_wallet_ffi_transfer_shielded() -> Result<()> {
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let from: FfiBytes32 = (&ctx.ctx().existing_public_accounts()[0]).into(); let from: FfiBytes32 = (&ctx.ctx().existing_public_accounts()[0]).into();
let (to, to_keys) = unsafe { let (to, to_keys) = unsafe {
let mut out_account_id = FfiBytes32::default();
let mut out_keys = FfiPrivateAccountKeys::default(); let mut out_keys = FfiPrivateAccountKeys::default();
wallet_ffi_create_account_private(wallet_ffi_handle, &raw mut out_account_id); wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys);
wallet_ffi_get_private_account_keys( let account_id = nssa::AccountId::from((&out_keys.npk(), 0_u128));
wallet_ffi_handle, let to: FfiBytes32 = (&account_id).into();
&raw const out_account_id, (to, out_keys)
&raw mut out_keys,
);
(out_account_id, out_keys)
}; };
let amount: [u8; 16] = 100_u128.to_le_bytes(); let amount: [u8; 16] = 100_u128.to_le_bytes();
@ -970,15 +941,11 @@ fn test_wallet_ffi_transfer_private() -> Result<()> {
let from: FfiBytes32 = (&ctx.ctx().existing_private_accounts()[0]).into(); let from: FfiBytes32 = (&ctx.ctx().existing_private_accounts()[0]).into();
let (to, to_keys) = unsafe { let (to, to_keys) = unsafe {
let mut out_account_id = FfiBytes32::default();
let mut out_keys = FfiPrivateAccountKeys::default(); let mut out_keys = FfiPrivateAccountKeys::default();
wallet_ffi_create_account_private(wallet_ffi_handle, &raw mut out_account_id); wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys);
wallet_ffi_get_private_account_keys( let account_id = nssa::AccountId::from((&out_keys.npk(), 0_u128));
wallet_ffi_handle, let to: FfiBytes32 = (&account_id).into();
&raw const out_account_id, (to, out_keys)
&raw mut out_keys,
);
(out_account_id, out_keys)
}; };
let amount: [u8; 16] = 100_u128.to_le_bytes(); let amount: [u8; 16] = 100_u128.to_le_bytes();

View File

@ -256,11 +256,11 @@ impl KeyTree<ChildKeysPublic> {
} }
impl KeyTree<ChildKeysPrivate> { impl KeyTree<ChildKeysPrivate> {
pub fn generate_new_private_node(&mut self, parent_cci: &ChainIndex) -> Option<ChainIndex> { pub fn create_private_accounts_key_node(&mut self, parent_cci: &ChainIndex) -> Option<ChainIndex> {
self.generate_new_node(parent_cci) self.generate_new_node(parent_cci)
} }
pub fn generate_new_private_node_layered(&mut self) -> Option<ChainIndex> { pub fn create_private_accounts_key_node_layered(&mut self) -> Option<ChainIndex> {
self.generate_new_node_layered() self.generate_new_node_layered()
} }

View File

@ -121,19 +121,19 @@ impl NSSAUserData {
.or_else(|| self.public_key_tree.get_node(account_id).map(Into::into)) .or_else(|| self.public_key_tree.get_node(account_id).map(Into::into))
} }
/// Generates a new private key node and returns its `ChainIndex`. /// Creates a new receiving key node and returns its `ChainIndex`.
pub fn generate_new_privacy_preserving_transaction_key_chain( pub fn create_private_accounts_key(
&mut self, &mut self,
parent_cci: Option<ChainIndex>, parent_cci: Option<ChainIndex>,
) -> ChainIndex { ) -> ChainIndex {
match parent_cci { match parent_cci {
Some(parent_cci) => self Some(parent_cci) => self
.private_key_tree .private_key_tree
.generate_new_private_node(&parent_cci) .create_private_accounts_key_node(&parent_cci)
.expect("Parent must be present in a tree"), .expect("Parent must be present in a tree"),
None => self None => self
.private_key_tree .private_key_tree
.generate_new_private_node_layered() .create_private_accounts_key_node_layered()
.expect("Search for new node slot failed"), .expect("Search for new node slot failed"),
} }
} }
@ -222,7 +222,7 @@ mod tests {
let mut user_data = NSSAUserData::default(); let mut user_data = NSSAUserData::default();
let chain_index = user_data let chain_index = user_data
.generate_new_privacy_preserving_transaction_key_chain(Some(ChainIndex::root())); .create_private_accounts_key(Some(ChainIndex::root()));
let is_key_chain_generated = user_data.private_key_tree.key_map.contains_key(&chain_index); let is_key_chain_generated = user_data.private_key_tree.key_map.contains_key(&chain_index);
assert!(is_key_chain_generated); assert!(is_key_chain_generated);

View File

@ -7,7 +7,7 @@ use nssa::AccountId;
use crate::{ use crate::{
block_on, block_on,
error::{print_error, WalletFfiError}, error::{print_error, WalletFfiError},
types::{FfiAccount, FfiAccountList, FfiAccountListEntry, FfiBytes32, WalletHandle}, types::{FfiAccount, FfiAccountList, FfiAccountListEntry, FfiBytes32, FfiPrivateAccountKeys, WalletHandle},
wallet::get_wallet, wallet::get_wallet,
}; };
@ -61,32 +61,36 @@ pub unsafe extern "C" fn wallet_ffi_create_account_public(
/// Create a new private key node. /// Create a new private key node.
/// ///
/// Returns the nullifier public key (npk) to share with senders. Account IDs are /// Returns the nullifier public key (npk) and viewing public key (vpk) to share with
/// discovered later via sync when senders initialize accounts under this key. /// senders. Account IDs are discovered later via sync when senders initialize accounts
/// under this key.
/// ///
/// # Parameters /// # Parameters
/// - `handle`: Valid wallet handle /// - `handle`: Valid wallet handle
/// - `out_npk`: Output pointer for the nullifier public key (32 bytes) /// - `out_keys`: Output pointer for the key data (npk + vpk)
/// ///
/// # Returns /// # Returns
/// - `Success` on successful creation /// - `Success` on successful creation
/// - Error code on failure /// - Error code on failure
/// ///
/// # Memory
/// The keys structure must be freed with `wallet_ffi_free_private_account_keys()`.
///
/// # Safety /// # Safety
/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` /// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open`
/// - `out_npk` must be a valid pointer to a `FfiBytes32` struct /// - `out_keys` must be a valid pointer to a `FfiPrivateAccountKeys` struct
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn wallet_ffi_create_account_private( pub unsafe extern "C" fn wallet_ffi_create_private_accounts_key(
handle: *mut WalletHandle, handle: *mut WalletHandle,
out_npk: *mut FfiBytes32, out_keys: *mut FfiPrivateAccountKeys,
) -> WalletFfiError { ) -> WalletFfiError {
let wrapper = match get_wallet(handle) { let wrapper = match get_wallet(handle) {
Ok(w) => w, Ok(w) => w,
Err(e) => return e, Err(e) => return e,
}; };
if out_npk.is_null() { if out_keys.is_null() {
print_error("Null output pointer for npk"); print_error("Null output pointer for keys");
return WalletFfiError::NullPointer; return WalletFfiError::NullPointer;
} }
@ -98,7 +102,7 @@ pub unsafe extern "C" fn wallet_ffi_create_account_private(
} }
}; };
let chain_index = wallet.create_new_account_private(None); let chain_index = wallet.create_private_accounts_key(None);
let node = wallet let node = wallet
.storage() .storage()
@ -108,8 +112,20 @@ pub unsafe extern "C" fn wallet_ffi_create_account_private(
.get(&chain_index) .get(&chain_index)
.expect("Node was just inserted"); .expect("Node was just inserted");
let key_chain = &node.value.0;
let npk_bytes = key_chain.nullifier_public_key.0;
let vpk_bytes = key_chain.viewing_public_key.to_bytes();
let vpk_len = vpk_bytes.len();
#[expect(
clippy::as_conversions,
reason = "We need to convert the boxed slice into a raw pointer for FFI"
)]
let vpk_ptr = Box::into_raw(vpk_bytes.to_vec().into_boxed_slice()) as *const u8;
unsafe { unsafe {
(*out_npk).data = node.value.0.nullifier_public_key.0; (*out_keys).nullifier_public_key.data = npk_bytes;
(*out_keys).viewing_public_key = vpk_ptr;
(*out_keys).viewing_public_key_len = vpk_len;
} }
WalletFfiError::Success WalletFfiError::Success

View File

@ -126,6 +126,24 @@ typedef struct FfiBytes32 {
uint8_t data[32]; uint8_t data[32];
} FfiBytes32; } FfiBytes32;
/**
* Public keys for a private account (safe to expose).
*/
typedef struct FfiPrivateAccountKeys {
/**
* Nullifier public key (32 bytes).
*/
struct FfiBytes32 nullifier_public_key;
/**
* viewing public key (compressed secp256k1 point).
*/
const uint8_t *viewing_public_key;
/**
* Length of viewing public key (typically 33 bytes).
*/
uintptr_t viewing_public_key_len;
} FfiPrivateAccountKeys;
/** /**
* Single entry in the account list. * Single entry in the account list.
*/ */
@ -189,24 +207,6 @@ typedef struct FfiPublicAccountKey {
struct FfiBytes32 public_key; struct FfiBytes32 public_key;
} FfiPublicAccountKey; } FfiPublicAccountKey;
/**
* Public keys for a private account (safe to expose).
*/
typedef struct FfiPrivateAccountKeys {
/**
* Nullifier public key (32 bytes).
*/
struct FfiBytes32 nullifier_public_key;
/**
* viewing public key (compressed secp256k1 point).
*/
const uint8_t *viewing_public_key;
/**
* Length of viewing public key (typically 33 bytes).
*/
uintptr_t viewing_public_key_len;
} FfiPrivateAccountKeys;
/** /**
* Result of a transfer operation. * Result of a transfer operation.
*/ */
@ -245,23 +245,27 @@ enum WalletFfiError wallet_ffi_create_account_public(struct WalletHandle *handle
/** /**
* Create a new private key node. * Create a new private key node.
* *
* Returns the nullifier public key (npk) to share with senders. Account IDs are * Returns the nullifier public key (npk) and viewing public key (vpk) to share with
* discovered later via sync when senders initialize accounts under this key. * senders. Account IDs are discovered later via sync when senders initialize accounts
* under this key.
* *
* # Parameters * # Parameters
* - `handle`: Valid wallet handle * - `handle`: Valid wallet handle
* - `out_npk`: Output pointer for the nullifier public key (32 bytes) * - `out_keys`: Output pointer for the key data (npk + vpk)
* *
* # Returns * # Returns
* - `Success` on successful creation * - `Success` on successful creation
* - Error code on failure * - Error code on failure
* *
* # Memory
* The keys structure must be freed with `wallet_ffi_free_private_account_keys()`.
*
* # Safety * # Safety
* - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open`
* - `out_npk` must be a valid pointer to a `FfiBytes32` struct * - `out_keys` must be a valid pointer to a `FfiPrivateAccountKeys` struct
*/ */
enum WalletFfiError wallet_ffi_create_account_private(struct WalletHandle *handle, enum WalletFfiError wallet_ffi_create_private_accounts_key(struct WalletHandle *handle,
struct FfiBytes32 *out_npk); struct FfiPrivateAccountKeys *out_keys);
/** /**
* List all accounts in the wallet. * List all accounts in the wallet.

View File

@ -82,8 +82,8 @@ pub enum NewSubcommand {
/// Label to assign to the new account. /// Label to assign to the new account.
label: Option<String>, label: Option<String>,
}, },
/// Register new private account. /// Create a new receiving key (npk + vpk) to share with senders.
Private { PrivateAccountsKey {
#[arg(long)] #[arg(long)]
/// Chain index of a parent node. /// Chain index of a parent node.
cci: Option<ChainIndex>, cci: Option<ChainIndex>,
@ -133,8 +133,8 @@ impl WalletSubcommand for NewSubcommand {
Ok(SubcommandReturnValue::RegisterAccount { account_id }) Ok(SubcommandReturnValue::RegisterAccount { account_id })
} }
Self::Private { cci } => { Self::PrivateAccountsKey { cci } => {
let chain_index = wallet_core.create_new_account_private(cci); let chain_index = wallet_core.create_private_accounts_key(cci);
let node = wallet_core let node = wallet_core
.storage .storage

View File

@ -256,13 +256,13 @@ impl WalletCore {
.generate_new_public_transaction_private_key(chain_index) .generate_new_public_transaction_private_key(chain_index)
} }
pub fn create_new_account_private( pub fn create_private_accounts_key(
&mut self, &mut self,
chain_index: Option<ChainIndex>, chain_index: Option<ChainIndex>,
) -> ChainIndex { ) -> ChainIndex {
self.storage self.storage
.user_data .user_data
.generate_new_privacy_preserving_transaction_key_chain(chain_index) .create_private_accounts_key(chain_index)
} }
/// Get account balance. /// Get account balance.