fix: resolve merge conflicts

This commit is contained in:
Moudy 2026-04-30 15:04:33 +02:00
commit 4c28133448
89 changed files with 2003 additions and 839 deletions

10
Cargo.lock generated
View File

@ -2109,7 +2109,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@ -2410,7 +2410,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@ -7105,7 +7105,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@ -8038,7 +8038,7 @@ dependencies = [
"getrandom 0.4.2",
"once_cell",
"rustix",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@ -9321,7 +9321,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -93,6 +93,12 @@ Only `Public/2gJJjtG9UivBGEhA1Jz6waZQx1cwfYupC5yvKEweHaeH` is used for completio
exec zsh
```
> **Note:** After updating the completion script, re-run step 1 to copy the new file, then rebuild the cache:
> ```sh
> cp _wallet ~/.oh-my-zsh/custom/plugins/wallet/
> rm -rf ~/.zcompdump* && exec zsh
> ```
### Requirements
The completion script calls `wallet account list` to dynamically fetch account IDs. Ensure the `wallet` command is in your `$PATH`.
@ -197,8 +203,7 @@ wallet account get --account-id <TAB>
2. Rebuild the completion cache:
```sh
rm -f ~/.zcompdump*
exec zsh
rm -rf ~/.zcompdump* && exec zsh
```
### Account IDs not completing

View File

@ -46,7 +46,7 @@ _wallet() {
cword=$COMP_CWORD
}
local commands="auth-transfer chain-info account pinata token amm check-health config restore-keys deploy-program help"
local commands="auth-transfer chain-info account pinata token amm ata check-health config restore-keys deploy-program help"
# Find the main command and subcommand by scanning words before the cursor.
# Global options that take a value are skipped along with their argument.
@ -127,10 +127,10 @@ _wallet() {
--to-label)
_wallet_complete_account_label "$cur"
;;
--to-npk | --to-vpk | --amount)
--to-npk | --to-vpk | --to-identifier | --amount)
;; # no specific completion
*)
COMPREPLY=($(compgen -W "--from --from-label --to --to-label --to-npk --to-vpk --amount" -- "$cur"))
COMPREPLY=($(compgen -W "--from --from-label --to --to-label --to-npk --to-vpk --to-identifier --amount" -- "$cur"))
;;
esac
;;
@ -187,11 +187,11 @@ _wallet() {
sync-private)
;; # no options
new)
# `account new` is itself a subcommand: public | private
# `account new` is itself a subcommand: public | private-accounts-key
local new_subcmd=""
for ((i = subcmd_idx + 1; i < cword; i++)); do
case "${words[$i]}" in
public | private)
public | private-accounts-key)
new_subcmd="${words[$i]}"
break
;;
@ -199,13 +199,26 @@ _wallet() {
done
if [[ -z "$new_subcmd" ]]; then
COMPREPLY=($(compgen -W "public private" -- "$cur"))
COMPREPLY=($(compgen -W "public private-accounts-key" -- "$cur"))
else
case "$prev" in
--cci | -l | --label)
;; # no specific completion
*)
COMPREPLY=($(compgen -W "--cci -l --label" -- "$cur"))
case "$new_subcmd" in
public)
case "$prev" in
--cci | -l | --label)
;; # no specific completion
*)
COMPREPLY=($(compgen -W "--cci -l --label" -- "$cur"))
;;
esac
;;
private-accounts-key)
case "$prev" in
--cci)
;; # no specific completion
*)
COMPREPLY=($(compgen -W "--cci" -- "$cur"))
;;
esac
;;
esac
fi
@ -289,10 +302,10 @@ _wallet() {
--to-label)
_wallet_complete_account_label "$cur"
;;
--to-npk | --to-vpk | --amount)
--to-npk | --to-vpk | --to-identifier | --amount)
;; # no specific completion
*)
COMPREPLY=($(compgen -W "--from --from-label --to --to-label --to-npk --to-vpk --amount" -- "$cur"))
COMPREPLY=($(compgen -W "--from --from-label --to --to-label --to-npk --to-vpk --to-identifier --amount" -- "$cur"))
;;
esac
;;
@ -331,10 +344,10 @@ _wallet() {
--holder-label)
_wallet_complete_account_label "$cur"
;;
--holder-npk | --holder-vpk | --amount)
--holder-npk | --holder-vpk | --holder-identifier | --amount)
;; # no specific completion
*)
COMPREPLY=($(compgen -W "--definition --definition-label --holder --holder-label --holder-npk --holder-vpk --amount" -- "$cur"))
COMPREPLY=($(compgen -W "--definition --definition-label --holder --holder-label --holder-npk --holder-vpk --holder-identifier --amount" -- "$cur"))
;;
esac
;;
@ -344,7 +357,7 @@ _wallet() {
amm)
case "$subcmd" in
"")
COMPREPLY=($(compgen -W "new swap add-liquidity remove-liquidity help" -- "$cur"))
COMPREPLY=($(compgen -W "new swap-exact-input swap-exact-output add-liquidity remove-liquidity help" -- "$cur"))
;;
new)
case "$prev" in
@ -373,7 +386,7 @@ _wallet() {
;;
esac
;;
swap)
swap-exact-input)
case "$prev" in
--user-holding-a)
_wallet_complete_account_id "$cur"
@ -394,6 +407,15 @@ _wallet() {
;;
esac
;;
swap-exact-output)
case "$prev" in
--user-holding-a | --user-holding-b | --exact-amount-out | --max-amount-in | --token-definition)
;; # no specific completion
*)
COMPREPLY=($(compgen -W "--user-holding-a --user-holding-b --exact-amount-out --max-amount-in --token-definition" -- "$cur"))
;;
esac
;;
add-liquidity)
case "$prev" in
--user-holding-a)
@ -451,6 +473,68 @@ _wallet() {
esac
;;
ata)
case "$subcmd" in
"")
COMPREPLY=($(compgen -W "address create send burn list help" -- "$cur"))
;;
address)
case "$prev" in
--owner | --token-definition)
;; # no specific completion
*)
COMPREPLY=($(compgen -W "--owner --token-definition" -- "$cur"))
;;
esac
;;
create)
case "$prev" in
--owner)
_wallet_complete_account_id "$cur"
;;
--token-definition)
;; # no specific completion
*)
COMPREPLY=($(compgen -W "--owner --token-definition" -- "$cur"))
;;
esac
;;
send)
case "$prev" in
--from)
_wallet_complete_account_id "$cur"
;;
--to | --token-definition | --amount)
;; # no specific completion
*)
COMPREPLY=($(compgen -W "--from --token-definition --to --amount" -- "$cur"))
;;
esac
;;
burn)
case "$prev" in
--holder)
_wallet_complete_account_id "$cur"
;;
--token-definition | --amount)
;; # no specific completion
*)
COMPREPLY=($(compgen -W "--holder --token-definition --amount" -- "$cur"))
;;
esac
;;
list)
case "$prev" in
--owner | --token-definition)
;; # no specific completion
*)
COMPREPLY=($(compgen -W "--owner --token-definition" -- "$cur"))
;;
esac
;;
esac
;;
config)
case "$subcmd" in
"")

View File

@ -24,6 +24,7 @@ _wallet() {
'pinata:Pinata program interaction subcommand'
'token:Token program interaction subcommand'
'amm:AMM program interaction subcommand'
'ata:Associated Token Account program interaction subcommand'
'check-health:Check the wallet can connect to the node and builtin local programs match the remote versions'
'config:Command to setup config, get and set config fields'
'restore-keys:Restoring keys from given password at given depth'
@ -52,6 +53,9 @@ _wallet() {
amm)
_wallet_amm
;;
ata)
_wallet_ata
;;
config)
_wallet_config
;;
@ -72,7 +76,7 @@ _wallet() {
# auth-transfer subcommand
_wallet_auth_transfer() {
local -a subcommands
_arguments -C \
'1: :->subcommand' \
'*:: :->args'
@ -91,16 +95,17 @@ _wallet_auth_transfer() {
init)
_arguments \
'--account-id[Account ID to initialize]:account_id:_wallet_account_ids' \
'--account-label[Account label (alternative to --account-id)]:label:_wallet_account_labels'
'--account-label[Account label (alternative to --account-id)]:label:'
;;
send)
_arguments \
'--from[Source account ID]:from_account:_wallet_account_ids' \
'--from-label[Source account label (alternative to --from)]:label:_wallet_account_labels' \
'--from-label[From account label (alternative to --from)]:label:' \
'--to[Destination account ID (for owned accounts)]:to_account:_wallet_account_ids' \
'--to-label[Destination account label (alternative to --to)]:label:_wallet_account_labels' \
'--to-label[To account label (alternative to --to)]:label:' \
'--to-npk[Destination nullifier public key (for foreign private accounts)]:npk:' \
'--to-vpk[Destination viewing public key (for foreign private accounts)]:vpk:' \
'--to-identifier[Identifier for the recipient private account]:identifier:' \
'--amount[Amount of native tokens to send]:amount:'
;;
esac
@ -111,7 +116,7 @@ _wallet_auth_transfer() {
# chain-info subcommand
_wallet_chain_info() {
local -a subcommands
_arguments -C \
'1: :->subcommand' \
'*:: :->args'
@ -144,7 +149,7 @@ _wallet_chain_info() {
# account subcommand
_wallet_account() {
local -a subcommands
_arguments -C \
'1: :->subcommand' \
'*:: :->args'
@ -169,7 +174,7 @@ _wallet_account() {
'(-r --raw)'{-r,--raw}'[Get raw account data]' \
'(-k --keys)'{-k,--keys}'[Display keys (pk for public accounts, npk/vpk for private accounts)]' \
'(-a --account-id)'{-a,--account-id}'[Account ID to query]:account_id:_wallet_account_ids' \
'--account-label[Account label (alternative to --account-id)]:label:_wallet_account_labels'
'--account-label[Account label (alternative to --account-id)]:label:'
;;
list|ls)
_arguments \
@ -181,19 +186,27 @@ _wallet_account() {
'*:: :->new_args'
case $state in
account_type)
compadd public private
compadd public private-accounts-key
;;
new_args)
_arguments \
'--cci[Chain index of a parent node]:chain_index:' \
'(-l --label)'{-l,--label}'[Label to assign to the new account]:label:'
case $line[1] in
public)
_arguments \
'--cci[Chain index of a parent node]:chain_index:' \
'(-l --label)'{-l,--label}'[Label to assign to the new account]:label:'
;;
private-accounts-key)
_arguments \
'--cci[Chain index of a parent node]:chain_index:'
;;
esac
;;
esac
;;
label)
_arguments \
'(-a --account-id)'{-a,--account-id}'[Account ID to label]:account_id:_wallet_account_ids' \
'--account-label[Account label (alternative to --account-id)]:label:_wallet_account_labels' \
'--account-label[Account label (alternative to --account-id)]:label:' \
'(-l --label)'{-l,--label}'[The label to assign to the account]:label:'
;;
esac
@ -204,7 +217,7 @@ _wallet_account() {
# pinata subcommand
_wallet_pinata() {
local -a subcommands
_arguments -C \
'1: :->subcommand' \
'*:: :->args'
@ -222,7 +235,7 @@ _wallet_pinata() {
claim)
_arguments \
'--to[Destination account ID to receive claimed tokens]:to_account:_wallet_account_ids' \
'--to-label[Destination account label (alternative to --to)]:label:_wallet_account_labels'
'--to-label[To account label (alternative to --to)]:label:'
;;
esac
;;
@ -255,36 +268,38 @@ _wallet_token() {
'--name[Token name]:name:' \
'--total-supply[Total supply of tokens to mint]:total_supply:' \
'--definition-account-id[Account ID for token definition]:definition_account:_wallet_account_ids' \
'--definition-account-label[Definition account label (alternative to --definition-account-id)]:label:_wallet_account_labels' \
'--definition-account-label[Definition account label (alternative to --definition-account-id)]:label:' \
'--supply-account-id[Account ID to receive initial supply]:supply_account:_wallet_account_ids' \
'--supply-account-label[Supply account label (alternative to --supply-account-id)]:label:_wallet_account_labels'
'--supply-account-label[Supply account label (alternative to --supply-account-id)]:label:'
;;
send)
_arguments \
'--from[Source holding account ID]:from_account:_wallet_account_ids' \
'--from-label[Source account label (alternative to --from)]:label:_wallet_account_labels' \
'--from-label[From account label (alternative to --from)]:label:' \
'--to[Destination holding account ID (for owned accounts)]:to_account:_wallet_account_ids' \
'--to-label[Destination account label (alternative to --to)]:label:_wallet_account_labels' \
'--to-label[To account label (alternative to --to)]:label:' \
'--to-npk[Destination nullifier public key (for foreign private accounts)]:npk:' \
'--to-vpk[Destination viewing public key (for foreign private accounts)]:vpk:' \
'--to-identifier[Identifier for the recipient private account]:identifier:' \
'--amount[Amount of tokens to send]:amount:'
;;
burn)
_arguments \
'--definition[Definition account ID]:definition_account:_wallet_account_ids' \
'--definition-label[Definition account label (alternative to --definition)]:label:_wallet_account_labels' \
'--definition-label[Definition account label (alternative to --definition)]:label:' \
'--holder[Holder account ID]:holder_account:_wallet_account_ids' \
'--holder-label[Holder account label (alternative to --holder)]:label:_wallet_account_labels' \
'--holder-label[Holder account label (alternative to --holder)]:label:' \
'--amount[Amount of tokens to burn]:amount:'
;;
mint)
_arguments \
'--definition[Definition account ID]:definition_account:_wallet_account_ids' \
'--definition-label[Definition account label (alternative to --definition)]:label:_wallet_account_labels' \
'--definition-label[Definition account label (alternative to --definition)]:label:' \
'--holder[Holder account ID (for owned accounts)]:holder_account:_wallet_account_ids' \
'--holder-label[Holder account label (alternative to --holder)]:label:_wallet_account_labels' \
'--holder-label[Holder account label (alternative to --holder)]:label:' \
'--holder-npk[Holder nullifier public key (for foreign private accounts)]:npk:' \
'--holder-vpk[Holder viewing public key (for foreign private accounts)]:vpk:' \
'--holder-identifier[Identifier for the holder private account]:identifier:' \
'--amount[Amount of tokens to mint]:amount:'
;;
esac
@ -295,7 +310,7 @@ _wallet_token() {
# amm subcommand
_wallet_amm() {
local -a subcommands
_arguments -C \
'1: :->subcommand' \
'*:: :->args'
@ -304,7 +319,8 @@ _wallet_amm() {
subcommand)
subcommands=(
'new:Create a new liquidity pool'
'swap:Swap tokens using the AMM'
'swap-exact-input:Swap specifying exact input amount'
'swap-exact-output:Swap specifying exact output amount'
'add-liquidity:Add liquidity to an existing pool'
'remove-liquidity:Remove liquidity from a pool'
'help:Print this message or the help of the given subcommand(s)'
@ -316,32 +332,40 @@ _wallet_amm() {
new)
_arguments \
'--user-holding-a[User token A holding account ID]:holding_a:_wallet_account_ids' \
'--user-holding-a-label[User holding A account label (alternative to --user-holding-a)]:label:_wallet_account_labels' \
'--user-holding-a-label[User holding A label (alternative to --user-holding-a)]:label:' \
'--user-holding-b[User token B holding account ID]:holding_b:_wallet_account_ids' \
'--user-holding-b-label[User holding B account label (alternative to --user-holding-b)]:label:_wallet_account_labels' \
'--user-holding-b-label[User holding B label (alternative to --user-holding-b)]:label:' \
'--user-holding-lp[User LP token holding account ID]:holding_lp:_wallet_account_ids' \
'--user-holding-lp-label[User holding LP account label (alternative to --user-holding-lp)]:label:_wallet_account_labels' \
'--user-holding-lp-label[User holding LP label (alternative to --user-holding-lp)]:label:' \
'--balance-a[Amount of token A to deposit]:balance_a:' \
'--balance-b[Amount of token B to deposit]:balance_b:'
;;
swap)
swap-exact-input)
_arguments \
'--user-holding-a[User token A holding account ID]:holding_a:_wallet_account_ids' \
'--user-holding-a-label[User holding A account label (alternative to --user-holding-a)]:label:_wallet_account_labels' \
'--user-holding-a-label[User holding A label (alternative to --user-holding-a)]:label:' \
'--user-holding-b[User token B holding account ID]:holding_b:_wallet_account_ids' \
'--user-holding-b-label[User holding B account label (alternative to --user-holding-b)]:label:_wallet_account_labels' \
'--user-holding-b-label[User holding B label (alternative to --user-holding-b)]:label:' \
'--amount-in[Amount of tokens to swap]:amount_in:' \
'--min-amount-out[Minimum tokens expected in return]:min_amount_out:' \
'--token-definition[Definition ID of the token being provided]:token_def:'
;;
swap-exact-output)
_arguments \
'--user-holding-a[User token A holding account ID]:holding_a:' \
'--user-holding-b[User token B holding account ID]:holding_b:' \
'--exact-amount-out[Exact amount of tokens expected out]:exact_amount_out:' \
'--max-amount-in[Maximum tokens to spend]:max_amount_in:' \
'--token-definition[Definition ID of the token being provided]:token_def:'
;;
add-liquidity)
_arguments \
'--user-holding-a[User token A holding account ID]:holding_a:_wallet_account_ids' \
'--user-holding-a-label[User holding A account label (alternative to --user-holding-a)]:label:_wallet_account_labels' \
'--user-holding-a-label[User holding A label (alternative to --user-holding-a)]:label:' \
'--user-holding-b[User token B holding account ID]:holding_b:_wallet_account_ids' \
'--user-holding-b-label[User holding B account label (alternative to --user-holding-b)]:label:_wallet_account_labels' \
'--user-holding-b-label[User holding B label (alternative to --user-holding-b)]:label:' \
'--user-holding-lp[User LP token holding account ID]:holding_lp:_wallet_account_ids' \
'--user-holding-lp-label[User holding LP account label (alternative to --user-holding-lp)]:label:_wallet_account_labels' \
'--user-holding-lp-label[User holding LP label (alternative to --user-holding-lp)]:label:' \
'--max-amount-a[Maximum amount of token A to deposit]:max_amount_a:' \
'--max-amount-b[Maximum amount of token B to deposit]:max_amount_b:' \
'--min-amount-lp[Minimum LP tokens to receive]:min_amount_lp:'
@ -349,11 +373,11 @@ _wallet_amm() {
remove-liquidity)
_arguments \
'--user-holding-a[User token A holding account ID]:holding_a:_wallet_account_ids' \
'--user-holding-a-label[User holding A account label (alternative to --user-holding-a)]:label:_wallet_account_labels' \
'--user-holding-a-label[User holding A label (alternative to --user-holding-a)]:label:' \
'--user-holding-b[User token B holding account ID]:holding_b:_wallet_account_ids' \
'--user-holding-b-label[User holding B account label (alternative to --user-holding-b)]:label:_wallet_account_labels' \
'--user-holding-b-label[User holding B label (alternative to --user-holding-b)]:label:' \
'--user-holding-lp[User LP token holding account ID]:holding_lp:_wallet_account_ids' \
'--user-holding-lp-label[User holding LP account label (alternative to --user-holding-lp)]:label:_wallet_account_labels' \
'--user-holding-lp-label[User holding LP label (alternative to --user-holding-lp)]:label:' \
'--balance-lp[Amount of LP tokens to burn]:balance_lp:' \
'--min-amount-a[Minimum token A to receive]:min_amount_a:' \
'--min-amount-b[Minimum token B to receive]:min_amount_b:'
@ -363,6 +387,61 @@ _wallet_amm() {
esac
}
# ata subcommand
_wallet_ata() {
local -a subcommands
_arguments -C \
'1: :->subcommand' \
'*:: :->args'
case $state in
subcommand)
subcommands=(
'address:Derive and print the Associated Token Account address (local only)'
'create:Create (or idempotently no-op) the Associated Token Account'
'send:Send tokens from owner ATA to a recipient token holding account'
'burn:Burn tokens from holder ATA'
'list:List all ATAs for a given owner across multiple token definitions'
'help:Print this message or the help of the given subcommand(s)'
)
_describe -t subcommands 'ata subcommands' subcommands
;;
args)
case $line[1] in
address)
_arguments \
'--owner[Owner account (no privacy prefix)]:owner:' \
'--token-definition[Token definition account (no privacy prefix)]:token_def:'
;;
create)
_arguments \
'--owner[Owner account with privacy prefix]:owner:_wallet_account_ids' \
'--token-definition[Token definition account (no privacy prefix)]:token_def:'
;;
send)
_arguments \
'--from[Sender account with privacy prefix]:from:_wallet_account_ids' \
'--token-definition[Token definition account (no privacy prefix)]:token_def:' \
'--to[Recipient account (no privacy prefix)]:to:' \
'--amount[Amount of tokens to send]:amount:'
;;
burn)
_arguments \
'--holder[Holder account with privacy prefix]:holder:_wallet_account_ids' \
'--token-definition[Token definition account (no privacy prefix)]:token_def:' \
'--amount[Amount of tokens to burn]:amount:'
;;
list)
_arguments \
'--owner[Owner account (no privacy prefix)]:owner:' \
'--token-definition[Token definition accounts (no privacy prefix)]:token_def:'
;;
esac
;;
esac
}
# config subcommand
_wallet_config() {
local -a subcommands
@ -435,6 +514,7 @@ _wallet_help() {
'pinata:Pinata program interaction subcommand'
'token:Token program interaction subcommand'
'amm:AMM program interaction subcommand'
'ata:Associated Token Account program interaction subcommand'
'check-health:Check the wallet can connect to the node'
'config:Command to setup config, get and set config fields'
'restore-keys:Restoring keys from given password at given depth'
@ -468,25 +548,4 @@ _wallet_account_ids() {
_multi_parts / accounts
}
# Helper function to complete account labels
# Uses `wallet account list` to get available labels
_wallet_account_labels() {
local -a labels
local line
if command -v wallet &>/dev/null; then
while IFS= read -r line; do
local label
# Extract label from [...] at end of line
label="${line##*\[}"
label="${label%\]}"
[[ -n "$label" && "$label" != "$line" ]] && labels+=("$label")
done < <(wallet account list 2>/dev/null)
fi
if (( ${#labels} > 0 )); then
compadd -a labels
fi
}
_wallet "$@"

View File

@ -5,6 +5,7 @@ This tutorial walks through native token transfers between public and private ac
4. Private account creation.
5. Native token transfer from a public account to a private account.
6. Native token transfer from a public account to a private account owned by someone else.
7. Sending to a private accounts key from multiple independent senders.
---
@ -142,7 +143,7 @@ Account owned by authenticated-transfer program
> Private accounts are structurally identical to public accounts, but their values are stored off-chain. On-chain, only a 32-byte commitment is recorded.
> Transactions include encrypted private values so the owner can recover them, and the decryption keys are never shared.
> Private accounts use two keypairs: nullifier keys for privacy-preserving executions and viewing keys for encrypting and decrypting values.
> The private account ID is derived from the nullifier public key.
> The private account ID is derived from the nullifier public key and a numeric identifier: `SHA256(prefix || npk || identifier)`. The same `npk` paired with different identifiers yields different, independent account IDs.
> Private accounts can be initialized by anyone, but once initialized they can only be modified by the owners keys.
> Updates include a new commitment and a nullifier for the old state, which prevents linkage between versions.
@ -158,7 +159,9 @@ With vpk 02ddc96d0eb56e00ce14994cfdaec5ae1f76244180a919545983156e3519940a17
```
> [!Tip]
> Focus on the account ID for now. The `npk` and `vpk` values are stored locally and used to build privacy-preserving transactions. The private account ID is derived from `npk`.
> Save this account ID. You will use it in later commands.
### b. Check the account status
Just like public accounts, new private accounts start out uninitialized:
@ -218,21 +221,23 @@ Account owned by authenticated-transfer program
## 6. Native token transfer from a public account to a private account owned by someone else
> [!Important]
> Well simulate transferring to someone else by creating a new private account we own and treating it as if it belonged to another user.
> Well simulate transferring to someone else by creating a new private accounts key and treating it as if it belonged to another user. When the recipient is someone else, you only have their `npk` and `vpk` — not an account ID.
### a. Create a new uninitialized private account
### a. Create a new private accounts key to simulate a foreign recipient
```bash
wallet account new private
wallet account new private-accounts-key
# Output:
Generated new account with account_id Private/AukXPRBmrYVqoqEW2HTs7N3hvTn3qdNFDcxDHVr5hMm5
Generated new private accounts key at path /1
With npk 0c95ebc4b3830f53da77bb0b80a276a776cdcf6410932acc718dcdb3f788a00e
With vpk 039fd12a3674a880d3e917804129141e4170d419d1f9e28a3dcf979c1f2369cb72
```
> [!Tip]
> Ignore the private account ID here and use the `npk` and `vpk` values to send to a foreign private account.
> Ignore the account ID here and use the `npk` and `vpk` values to send to a foreign private account.
### b. Send 3 tokens using the recipients npk and vpk
```bash
wallet auth-transfer send \
@ -242,9 +247,74 @@ wallet auth-transfer send \
--amount 3
```
> [!Note]
> `--to-identifier` is omitted here. When omitted, the wallet picks a random identifier, which is usually fine. Use the flag explicitly when a specific identifier is required.
> [!Warning]
> This command creates a privacy-preserving transaction, which may take a few minutes. The updated values are encrypted and included in the transaction.
> Once accepted, the recipient must run `wallet account sync-private` to scan the chain for their encrypted updates and refresh local state.
> [!Note]
> You have seen transfers between two public accounts and from a public sender to a private recipient. Transfers from a private sender, whether to a public account or to another private account, follow the same pattern.
## 7. Sending to a private accounts key from multiple independent senders
> [!Important]
> A private accounts key (`npk` + `vpk`) can be shared with multiple senders. Each sender independently chooses an identifier; the recipient's account ID is derived from `(npk, identifier)`. Two senders using different identifiers produce two separate private accounts under the same key.
### a. Alice creates a private accounts key
```bash
wallet account new private-accounts-key
# Output:
Generated new private accounts key at path /2
With npk a3f7c21b8e905d4f6a1bc783d0e2f94c1d5a6b7e8f9012345678abcdef012345
With vpk 03b1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6071819202122232425262728292a2b2c
```
Alice shares the `npk` and `vpk` values with Bob and Charlie out of band.
### b. Bob sends 10 tokens to Alice using identifier 1
```bash
wallet auth-transfer send \
--from Public/BobXqJprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPA \
--to-npk a3f7c21b8e905d4f6a1bc783d0e2f94c1d5a6b7e8f9012345678abcdef012345 \
--to-vpk 03b1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6071819202122232425262728292a2b2c \
--to-identifier 1 \
--amount 10
```
### c. Charlie sends 5 tokens to Alice using identifier 2
```bash
wallet auth-transfer send \
--from Public/CharlieYrP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPB \
--to-npk a3f7c21b8e905d4f6a1bc783d0e2f94c1d5a6b7e8f9012345678abcdef012345 \
--to-vpk 03b1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6071819202122232425262728292a2b2c \
--to-identifier 2 \
--amount 5
```
> [!Note]
> Bob and Charlie each chose a different identifier. They do not need to coordinate — any two distinct values work.
### d. Alice syncs to discover the new accounts
```bash
wallet account sync-private
```
```bash
wallet account list
# Output (private account entries under key /2):
/2 Private/AliceBobAcctXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
/2 Private/AliceCharlieAcctXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
Alice now has two separate private accounts, one funded by Bob and one by Charlie, both controlled by the same key at path `/2`.
> [!Tip]
> Alice can check each account balance with `wallet account get --account-id Private/...`. Neither balance is visible on-chain.

View File

@ -63,6 +63,7 @@ impl IndexerCore {
.iter()
.map(|init_comm_data| {
let npk = &init_comm_data.npk;
let account_id = nssa::AccountId::from((npk, 0));
let mut acc = init_comm_data.account.clone();
@ -70,8 +71,8 @@ impl IndexerCore {
nssa::program::Program::authenticated_transfer_program().id();
(
nssa_core::Commitment::new(npk, &acc),
nssa_core::Nullifier::for_account_initialization(npk),
nssa_core::Commitment::new(&account_id, &acc),
nssa_core::Nullifier::for_account_initialization(&account_id),
)
})
.collect()

View File

@ -60,11 +60,11 @@ impl InitialData {
let mut private_charlie_key_chain = KeyChain::new_os_random();
let mut private_charlie_account_id =
AccountId::from(&private_charlie_key_chain.nullifier_public_key);
AccountId::from((&private_charlie_key_chain.nullifier_public_key, 0));
let mut private_david_key_chain = KeyChain::new_os_random();
let mut private_david_account_id =
AccountId::from(&private_david_key_chain.nullifier_public_key);
AccountId::from((&private_david_key_chain.nullifier_public_key, 0));
// Ensure consistent ordering
if private_charlie_account_id > private_david_account_id {
@ -139,11 +139,10 @@ impl InitialData {
})
})
.chain(self.private_accounts.iter().map(|(key_chain, account)| {
let account_id = AccountId::from(&key_chain.nullifier_public_key);
InitialAccountData::Private(Box::new(PrivateAccountPrivateInitialData {
account_id,
account: account.clone(),
key_chain: key_chain.clone(),
identifier: 0,
}))
}))
.collect()

View File

@ -4,7 +4,7 @@
)]
use anyhow::Result;
use integration_tests::TestContext;
use integration_tests::{TestContext, format_private_account_id};
use log::info;
use nssa::program::Program;
use sequencer_service_rpc::RpcClient as _;
@ -70,34 +70,29 @@ async fn new_public_account_with_label() -> Result<()> {
}
#[test]
async fn new_private_account_with_label() -> Result<()> {
async fn add_label_to_existing_account() -> Result<()> {
let mut ctx = TestContext::new().await?;
let account_id = ctx.existing_private_accounts()[0];
let label = "my-test-private-account".to_owned();
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: None,
label: Some(label.clone()),
}));
let command = Command::Account(AccountSubcommand::Label {
account_id: Some(format_private_account_id(account_id)),
account_label: None,
label: label.clone(),
});
let result = execute_subcommand(ctx.wallet_mut(), command).await?;
execute_subcommand(ctx.wallet_mut(), command).await?;
// Extract the account_id from the result
let wallet::cli::SubcommandReturnValue::RegisterAccount { account_id } = result else {
panic!("Expected RegisterAccount return value")
};
// Verify the label was stored
let stored_label = ctx
.wallet()
.storage()
.labels
.get(&account_id.to_string())
.expect("Label should be stored for the new account");
.expect("Label should be stored for the account");
assert_eq!(stored_label.to_string(), label);
info!("Successfully created private account with label");
info!("Successfully set label on existing private account");
Ok(())
}

View File

@ -133,6 +133,7 @@ async fn amm_public() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 7,
};
@ -162,6 +163,7 @@ async fn amm_public() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 7,
};
@ -550,6 +552,7 @@ async fn amm_new_pool_using_labels() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 5,
};
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
@ -574,6 +577,7 @@ async fn amm_new_pool_using_labels() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 5,
};
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;

View File

@ -268,6 +268,7 @@ async fn transfer_and_burn_via_ata() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: fund_amount,
}),
)
@ -500,6 +501,7 @@ async fn transfer_via_ata_private_owner() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: fund_amount,
}),
)
@ -614,6 +616,7 @@ async fn burn_via_ata_private_owner() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: fund_amount,
}),
)

View File

@ -30,6 +30,7 @@ async fn private_transfer_to_owned_account() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 100,
});
@ -71,6 +72,7 @@ async fn private_transfer_to_foreign_account() -> Result<()> {
to_label: None,
to_npk: Some(to_npk_string),
to_vpk: Some(hex::encode(to_vpk.0)),
to_identifier: Some(0),
amount: 100,
});
@ -121,6 +123,7 @@ async fn deshielded_transfer_to_public_account() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 100,
});
@ -170,12 +173,11 @@ async fn private_transfer_to_owned_account_using_claiming_path() -> Result<()> {
};
// Get the keys for the newly created account
let (to_keys, _) = ctx
let (to_keys, _, to_identifier) = ctx
.wallet()
.storage()
.user_data
.get_private_account(to_account_id)
.cloned()
.context("Failed to get private account")?;
// Send to this account using claiming path (using npk and vpk instead of account ID)
@ -186,6 +188,7 @@ async fn private_transfer_to_owned_account_using_claiming_path() -> Result<()> {
to_label: None,
to_npk: Some(hex::encode(to_keys.nullifier_public_key.0)),
to_vpk: Some(hex::encode(to_keys.viewing_public_key.0)),
to_identifier: Some(to_identifier),
amount: 100,
});
@ -236,6 +239,7 @@ async fn shielded_transfer_to_owned_private_account() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 100,
});
@ -280,6 +284,7 @@ async fn shielded_transfer_to_foreign_account() -> Result<()> {
to_label: None,
to_npk: Some(to_npk_string),
to_vpk: Some(hex::encode(to_vpk.0)),
to_identifier: Some(0),
amount: 100,
});
@ -336,12 +341,11 @@ async fn private_transfer_to_owned_account_continuous_run_path() -> Result<()> {
};
// Get the newly created account's keys
let (to_keys, _) = ctx
let (to_keys, _, to_identifier) = ctx
.wallet()
.storage()
.user_data
.get_private_account(to_account_id)
.cloned()
.context("Failed to get private account")?;
// Send transfer using nullifier and viewing public keys
@ -352,6 +356,7 @@ async fn private_transfer_to_owned_account_continuous_run_path() -> Result<()> {
to_label: None,
to_npk: Some(hex::encode(to_keys.nullifier_public_key.0)),
to_vpk: Some(hex::encode(to_keys.viewing_public_key.0)),
to_identifier: Some(to_identifier),
amount: 100,
});
@ -455,6 +460,7 @@ async fn private_transfer_using_from_label() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 100,
});
@ -527,3 +533,112 @@ async fn initialize_private_account_using_label() -> Result<()> {
Ok(())
}
#[test]
async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> {
let mut ctx = TestContext::new().await?;
// Both transfers below will target this same node with distinct identifiers.
let chain_index = ctx.wallet_mut().create_private_accounts_key(None);
let (npk, vpk) = {
let node = ctx
.wallet()
.storage()
.user_data
.private_key_tree
.key_map
.get(&chain_index)
.expect("node was just inserted");
let key_chain = &node.value.0;
(
key_chain.nullifier_public_key,
key_chain.viewing_public_key.clone(),
)
};
let npk_hex = hex::encode(npk.0);
let vpk_hex = hex::encode(vpk.0);
let identifier_1 = 1_u128;
let identifier_2 = 2_u128;
let sender_0: AccountId = ctx.existing_public_accounts()[0];
let sender_1: AccountId = ctx.existing_public_accounts()[1];
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_public_account_id(sender_0)),
from_label: None,
to: None,
to_label: None,
to_npk: Some(npk_hex.clone()),
to_vpk: Some(vpk_hex.clone()),
to_identifier: Some(identifier_1),
amount: 100,
}),
)
.await?;
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_public_account_id(sender_1)),
from_label: None,
to: None,
to_label: None,
to_npk: Some(npk_hex),
to_vpk: Some(vpk_hex),
to_identifier: Some(identifier_2),
amount: 200,
}),
)
.await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::SyncPrivate {}),
)
.await?;
// Both accounts must be discovered with the correct balances.
let account_id_1 = AccountId::from((&npk, identifier_1));
let acc_1 = ctx
.wallet()
.get_account_private(account_id_1)
.context("account for identifier 1 not found after sync")?;
assert_eq!(acc_1.balance, 100);
let account_id_2 = AccountId::from((&npk, identifier_2));
let acc_2 = ctx
.wallet()
.get_account_private(account_id_2)
.context("account for identifier 2 not found after sync")?;
assert_eq!(acc_2.balance, 200);
// Both account ids must resolve to the same key node.
let tree = &ctx.wallet().storage().user_data.private_key_tree;
let ci_1 = tree
.account_id_map
.get(&account_id_1)
.context("account_id_1 missing from private_key_tree.account_id_map")?;
let ci_2 = tree
.account_id_map
.get(&account_id_2)
.context("account_id_2 missing from private_key_tree.account_id_map")?;
assert_eq!(
ci_1, ci_2,
"identifiers 1 and 2 under the same NPK must share a single chain_index"
);
assert_eq!(
ci_1, &chain_index,
"both accounts must resolve to the key node created at the start of the test"
);
info!("Successfully transferred to two distinct identifiers under the same NPK");
Ok(())
}

View File

@ -23,6 +23,7 @@ async fn successful_transfer_to_existing_account() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 100,
});
@ -81,6 +82,7 @@ pub async fn successful_transfer_to_new_account() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 100,
});
@ -119,6 +121,7 @@ async fn failed_transfer_with_insufficient_balance() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 1_000_000,
});
@ -159,6 +162,7 @@ async fn two_consecutive_successful_transfers() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 100,
});
@ -193,6 +197,7 @@ async fn two_consecutive_successful_transfers() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 100,
});
@ -278,6 +283,7 @@ async fn successful_transfer_using_from_label() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 100,
});
@ -325,6 +331,7 @@ async fn successful_transfer_using_to_label() -> Result<()> {
to_label: Some(label),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 100,
});

View File

@ -111,6 +111,7 @@ async fn indexer_state_consistency() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 100,
});
@ -147,6 +148,7 @@ async fn indexer_state_consistency() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 100,
});
@ -233,6 +235,7 @@ async fn indexer_state_consistency_with_labels() -> Result<()> {
to_label: Some(to_label_str),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 100,
});

View File

@ -92,6 +92,7 @@ fn indexer_ffi_state_consistency() -> Result<()> {
to_npk: None,
to_vpk: None,
amount: 100,
to_identifier: Some(0),
});
runtime_wrapped.block_on(wallet::cli::execute_subcommand(ctx.wallet_mut(), command))?;
@ -133,6 +134,7 @@ fn indexer_ffi_state_consistency() -> Result<()> {
to_npk: None,
to_vpk: None,
amount: 100,
to_identifier: Some(0),
});
runtime_wrapped.block_on(wallet::cli::execute_subcommand(ctx.wallet_mut(), command))?;
@ -235,6 +237,7 @@ fn indexer_ffi_state_consistency_with_labels() -> Result<()> {
to_npk: None,
to_vpk: None,
amount: 100,
to_identifier: Some(0),
});
runtime_wrapped.block_on(wallet::cli::execute_subcommand(ctx.wallet_mut(), command))?;

View File

@ -59,12 +59,11 @@ async fn sync_private_account_with_non_zero_chain_index() -> Result<()> {
};
// Get the keys for the newly created account
let (to_keys, _) = ctx
let (to_keys, _, to_identifier) = ctx
.wallet()
.storage()
.user_data
.get_private_account(to_account_id)
.cloned()
.context("Failed to get private account")?;
// Send to this account using claiming path (using npk and vpk instead of account ID)
@ -75,6 +74,7 @@ async fn sync_private_account_with_non_zero_chain_index() -> Result<()> {
to_label: None,
to_npk: Some(hex::encode(to_keys.nullifier_public_key.0)),
to_vpk: Some(hex::encode(to_keys.viewing_public_key.0)),
to_identifier: Some(to_identifier),
amount: 100,
});
@ -151,6 +151,7 @@ async fn restore_keys_from_seed() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 100,
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
@ -163,6 +164,7 @@ async fn restore_keys_from_seed() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 101,
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
@ -203,6 +205,7 @@ async fn restore_keys_from_seed() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 102,
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
@ -215,6 +218,7 @@ async fn restore_keys_from_seed() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 103,
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
@ -259,16 +263,16 @@ async fn restore_keys_from_seed() -> Result<()> {
.expect("Acc 4 should be restored");
assert_eq!(
acc1.value.1.program_owner,
acc1.value.1[0].1.program_owner,
Program::authenticated_transfer_program().id()
);
assert_eq!(
acc2.value.1.program_owner,
acc2.value.1[0].1.program_owner,
Program::authenticated_transfer_program().id()
);
assert_eq!(acc1.value.1.balance, 100);
assert_eq!(acc2.value.1.balance, 101);
assert_eq!(acc1.value.1[0].1.balance, 100);
assert_eq!(acc2.value.1[0].1.balance, 101);
info!("Tree checks passed, testing restored accounts can transact");
@ -280,6 +284,7 @@ async fn restore_keys_from_seed() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 10,
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
@ -291,6 +296,7 @@ async fn restore_keys_from_seed() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: 11,
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;

View File

@ -134,6 +134,7 @@ async fn create_and_transfer_public_token() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: transfer_amount,
};
@ -227,6 +228,7 @@ async fn create_and_transfer_public_token() -> Result<()> {
holder_label: None,
holder_npk: None,
holder_vpk: None,
holder_identifier: None,
amount: mint_amount,
};
@ -372,6 +374,7 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: transfer_amount,
};
@ -566,6 +569,7 @@ async fn create_token_with_private_definition() -> Result<()> {
holder_label: None,
holder_npk: None,
holder_vpk: None,
holder_identifier: None,
amount: mint_amount_public,
};
@ -614,6 +618,7 @@ async fn create_token_with_private_definition() -> Result<()> {
holder_label: None,
holder_npk: None,
holder_vpk: None,
holder_identifier: None,
amount: mint_amount_private,
};
@ -756,6 +761,7 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: transfer_amount,
};
@ -887,6 +893,7 @@ async fn shielded_token_transfer() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: transfer_amount,
};
@ -1013,6 +1020,7 @@ async fn deshielded_token_transfer() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: transfer_amount,
};
@ -1131,12 +1139,11 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> {
};
// Get keys for foreign mint (claiming path)
let (holder_keys, _) = ctx
let (holder_keys, _, holder_identifier) = ctx
.wallet()
.storage()
.user_data
.get_private_account(recipient_account_id)
.cloned()
.context("Failed to get private account keys")?;
// Mint using claiming path (foreign account)
@ -1148,6 +1155,7 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> {
holder_label: None,
holder_npk: Some(hex::encode(holder_keys.nullifier_public_key.0)),
holder_vpk: Some(hex::encode(holder_keys.viewing_public_key.0)),
holder_identifier: Some(holder_identifier),
amount: mint_amount,
};
@ -1351,6 +1359,7 @@ async fn transfer_token_using_from_label() -> Result<()> {
to_label: None,
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount: transfer_amount,
};
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;

View File

@ -220,14 +220,17 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
data: Data::default(),
},
true,
AccountId::from(&sender_npk),
AccountId::from((&sender_npk, 0)),
);
let recipient_nsk = [2; 32];
let recipient_vsk = [99; 32];
let recipient_vpk = ViewingPublicKey::from_scalar(recipient_vsk);
let recipient_npk = NullifierPublicKey::from(&recipient_nsk);
let recipient_pre =
AccountWithMetadata::new(Account::default(), false, AccountId::from(&recipient_npk));
let recipient_pre = AccountWithMetadata::new(
Account::default(),
false,
AccountId::from((&recipient_npk, 0)),
);
let eph_holder_from = EphemeralKeyHolder::new(&sender_npk);
let sender_ss = eph_holder_from.calculate_shared_secret_sender(&sender_vpk);
@ -249,7 +252,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
vec![sender_pre, recipient_pre],
Program::serialize_instruction(balance_to_move).unwrap(),
vec![1, 2],
vec![(sender_npk, sender_ss), (recipient_npk, recipient_ss)],
vec![(sender_npk, 0, sender_ss), (recipient_npk, 0, recipient_ss)],
vec![sender_nsk],
vec![Some(proof)],
&program.into(),

View File

@ -26,7 +26,7 @@ use nssa_core::program::DEFAULT_PROGRAM_ID;
use tempfile::tempdir;
use wallet_ffi::{
FfiAccount, FfiAccountList, FfiBytes32, FfiPrivateAccountKeys, FfiPublicAccountKey,
FfiTransferResult, WalletHandle, error,
FfiTransferResult, FfiU128, WalletHandle, error,
};
unsafe extern "C" {
@ -53,6 +53,11 @@ unsafe extern "C" {
out_account_id: *mut FfiBytes32,
) -> error::WalletFfiError;
fn wallet_ffi_create_private_accounts_key(
handle: *mut WalletHandle,
out_keys: *mut FfiPrivateAccountKeys,
) -> error::WalletFfiError;
fn wallet_ffi_list_accounts(
handle: *mut WalletHandle,
out_list: *mut FfiAccountList,
@ -116,6 +121,7 @@ unsafe extern "C" {
handle: *mut WalletHandle,
from: *const FfiBytes32,
to_keys: *const FfiPrivateAccountKeys,
to_identifier: *const FfiU128,
amount: *const [u8; 16],
out_result: *mut FfiTransferResult,
) -> error::WalletFfiError;
@ -132,6 +138,7 @@ unsafe extern "C" {
handle: *mut WalletHandle,
from: *const FfiBytes32,
to_keys: *const FfiPrivateAccountKeys,
to_identifier: *const FfiU128,
amount: *const [u8; 16],
out_result: *mut FfiTransferResult,
) -> error::WalletFfiError;
@ -260,33 +267,28 @@ fn wallet_ffi_create_public_accounts() -> Result<()> {
fn wallet_ffi_create_private_accounts() -> Result<()> {
let password = "password_for_tests";
let n_accounts = 10;
// Create `n_accounts` private accounts with wallet FFI
let new_private_account_ids_ffi = unsafe {
let mut account_ids = Vec::new();
// Create `n_accounts` receiving keys with wallet FFI
let new_npks_ffi = unsafe {
let mut npks = Vec::new();
let wallet_ffi_handle = new_wallet_ffi_with_default_config(password)?;
for _ in 0..n_accounts {
let mut out_account_id = FfiBytes32::from_bytes([0; 32]);
wallet_ffi_create_account_private(wallet_ffi_handle, &raw mut out_account_id);
account_ids.push(out_account_id.data);
let mut out_keys = FfiPrivateAccountKeys::default();
wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys);
npks.push(out_keys.nullifier_public_key.data);
wallet_ffi_free_private_account_keys(&raw mut out_keys);
}
wallet_ffi_destroy(wallet_ffi_handle);
account_ids
npks
};
// All returned IDs must be unique and non-zero
assert_eq!(new_private_account_ids_ffi.len(), n_accounts);
let unique: HashSet<_> = new_private_account_ids_ffi.iter().collect();
assert_eq!(
unique.len(),
n_accounts,
"Duplicate private account IDs returned"
);
// All returned NPKs must be unique and non-zero
assert_eq!(new_npks_ffi.len(), n_accounts);
let unique: HashSet<_> = new_npks_ffi.iter().collect();
assert_eq!(unique.len(), n_accounts, "Duplicate NPKs returned");
assert!(
new_private_account_ids_ffi
.iter()
.all(|id| *id != [0_u8; 32]),
"Zero account ID returned"
new_npks_ffi.iter().all(|id| *id != [0_u8; 32]),
"Zero NPK returned"
);
Ok(())
@ -294,46 +296,35 @@ fn wallet_ffi_create_private_accounts() -> Result<()> {
#[test]
fn wallet_ffi_save_and_load_persistent_storage() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let mut out_private_account_id = FfiBytes32::from_bytes([0; 32]);
let home = tempfile::tempdir()?;
// Create a private account with the wallet FFI and save it
unsafe {
// Create a receiving key and save
let first_npk = unsafe {
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);
let 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_destroy(wallet_ffi_handle);
}
let private_account_keys = unsafe {
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 result = wallet_ffi_get_private_account_keys(
wallet_ffi_handle,
&raw const out_private_account_id,
&raw mut out_keys,
);
assert_eq!(result, error::WalletFfiError::Success);
wallet_ffi_destroy(wallet_ffi_handle);
out_keys
npk
};
assert_eq!(
nssa::AccountId::from(&private_account_keys.npk()),
out_private_account_id.into()
// 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 mut out_keys = FfiPrivateAccountKeys::default();
wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys);
let npk = out_keys.nullifier_public_key.data;
wallet_ffi_free_private_account_keys(&raw mut out_keys);
wallet_ffi_destroy(wallet_ffi_handle);
npk
};
assert_ne!(first_npk, [0_u8; 32], "First NPK should be non-zero");
assert_ne!(second_npk, [0_u8; 32], "Second NPK should be non-zero");
assert_ne!(
first_npk, second_npk,
"Keys should differ after state was persisted"
);
Ok(())
@ -344,22 +335,22 @@ fn test_wallet_ffi_list_accounts() -> Result<()> {
let password = "password_for_tests";
// Create the wallet FFI and track which account IDs were created as public/private
let (wallet_ffi_handle, created_public_ids, created_private_ids) = unsafe {
let (wallet_ffi_handle, created_public_ids) = unsafe {
let handle = new_wallet_ffi_with_default_config(password)?;
let mut public_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 {
let mut out_account_id = FfiBytes32::from_bytes([0; 32]);
wallet_ffi_create_account_public(handle, &raw mut out_account_id);
public_ids.push(out_account_id.data);
wallet_ffi_create_account_private(handle, &raw mut out_account_id);
private_ids.push(out_account_id.data);
let mut out_keys = FfiPrivateAccountKeys::default();
wallet_ffi_create_private_accounts_key(handle, &raw mut out_keys);
wallet_ffi_free_private_account_keys(&raw mut out_keys);
}
(handle, public_ids, private_ids)
(handle, public_ids)
};
// Get the account list with FFI method
@ -382,31 +373,19 @@ fn test_wallet_ffi_list_accounts() -> Result<()> {
.filter(|e| e.is_public)
.map(|e| e.account_id.data)
.collect();
let listed_private_ids: HashSet<[u8; 32]> = wallet_ffi_account_list_slice
.iter()
.filter(|e| !e.is_public)
.map(|e| e.account_id.data)
.collect();
for id in &created_public_ids {
assert!(
listed_public_ids.contains(id),
"Created public account not found in list with is_public=true"
);
}
for id in &created_private_ids {
assert!(
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
// Total listed accounts must be at least the number of public accounts created
// (receiving keys without synced accounts don't appear in the list)
assert!(
wallet_ffi_account_list.count >= created_public_ids.len() + created_private_ids.len(),
"Listed account count ({}) is less than the number of created accounts ({})",
wallet_ffi_account_list.count >= created_public_ids.len(),
"Listed account count ({}) is less than the number of created public accounts ({})",
wallet_ffi_account_list.count,
created_public_ids.len() + created_private_ids.len()
created_public_ids.len()
);
unsafe {
@ -710,25 +689,13 @@ fn wallet_ffi_init_private_account_auth_transfer() -> Result<()> {
let home = tempfile::tempdir()?;
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
// Create a new uninitialized public account
let mut out_account_id = FfiBytes32::from_bytes([0; 32]);
// Create a new private account
let mut out_account_id = FfiBytes32::default();
unsafe {
wallet_ffi_create_account_private(wallet_ffi_handle, &raw mut out_account_id);
}
// Check its program owner is the default program id
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();
unsafe {
wallet_ffi_register_private_account(
@ -832,24 +799,24 @@ fn test_wallet_ffi_transfer_shielded() -> Result<()> {
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 (to, to_keys) = unsafe {
let mut out_account_id = FfiBytes32::default();
let mut out_keys = FfiPrivateAccountKeys::default();
wallet_ffi_create_account_private(wallet_ffi_handle, &raw mut out_account_id);
wallet_ffi_get_private_account_keys(
wallet_ffi_handle,
&raw const out_account_id,
&raw mut out_keys,
);
(out_account_id, out_keys)
wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys);
let account_id = nssa::AccountId::from((&out_keys.npk(), 0_u128));
let to: FfiBytes32 = (&account_id).into();
(to, out_keys)
};
let amount: [u8; 16] = 100_u128.to_le_bytes();
let mut transfer_result = FfiTransferResult::default();
unsafe {
let to_identifier = FfiU128 {
data: 0_u128.to_le_bytes(),
};
wallet_ffi_transfer_shielded(
wallet_ffi_handle,
&raw const from,
&raw const to_keys,
&raw const to_identifier,
&raw const amount,
&raw mut transfer_result,
);
@ -966,25 +933,25 @@ fn test_wallet_ffi_transfer_private() -> Result<()> {
let from: FfiBytes32 = (&ctx.ctx().existing_private_accounts()[0]).into();
let (to, to_keys) = unsafe {
let mut out_account_id = FfiBytes32::default();
let mut out_keys = FfiPrivateAccountKeys::default();
wallet_ffi_create_account_private(wallet_ffi_handle, &raw mut out_account_id);
wallet_ffi_get_private_account_keys(
wallet_ffi_handle,
&raw const out_account_id,
&raw mut out_keys,
);
(out_account_id, out_keys)
wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys);
let account_id = nssa::AccountId::from((&out_keys.npk(), 0_u128));
let to: FfiBytes32 = (&account_id).into();
(to, out_keys)
};
let amount: [u8; 16] = 100_u128.to_le_bytes();
let mut transfer_result = FfiTransferResult::default();
unsafe {
let to_identifier = FfiU128 {
data: 0_u128.to_le_bytes(),
};
wallet_ffi_transfer_private(
wallet_ffi_handle,
&raw const from,
&raw const to_keys,
&raw const to_identifier,
&raw const amount,
&raw mut transfer_result,
);

View File

@ -1,23 +1,24 @@
use k256::{Scalar, elliptic_curve::PrimeField as _};
use nssa_core::{NullifierPublicKey, encryption::ViewingPublicKey};
use nssa_core::{Identifier, NullifierPublicKey, encryption::ViewingPublicKey};
use serde::{Deserialize, Serialize};
use crate::key_management::{
KeyChain,
key_tree::traits::KeyNode,
key_tree::traits::KeyTreeNode,
secret_holders::{PrivateKeyHolder, SecretSpendingKey},
};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ChildKeysPrivate {
pub value: (KeyChain, nssa::Account),
pub value: (KeyChain, Vec<(Identifier, nssa::Account)>),
pub ccc: [u8; 32],
/// Can be [`None`] if root.
pub cci: Option<u32>,
}
impl KeyNode for ChildKeysPrivate {
fn root(seed: [u8; 64]) -> Self {
impl ChildKeysPrivate {
#[must_use]
pub fn root(seed: [u8; 64]) -> Self {
let hash_value = hmac_sha512::HMAC::mac(seed, b"LEE_master_priv");
let ssk = SecretSpendingKey(
@ -46,14 +47,15 @@ impl KeyNode for ChildKeysPrivate {
viewing_secret_key: vsk,
},
},
nssa::Account::default(),
vec![],
),
ccc,
cci: None,
}
}
fn nth_child(&self, cci: u32) -> Self {
#[must_use]
pub fn nth_child(&self, cci: u32) -> Self {
#[expect(clippy::arithmetic_side_effects, reason = "TODO: fix later")]
let parent_pt =
Scalar::from_repr(self.value.0.private_key_holder.nullifier_secret_key.into())
@ -95,43 +97,27 @@ impl KeyNode for ChildKeysPrivate {
viewing_secret_key: vsk,
},
},
nssa::Account::default(),
vec![],
),
ccc,
cci: Some(cci),
}
}
fn chain_code(&self) -> &[u8; 32] {
&self.ccc
}
fn child_index(&self) -> Option<u32> {
self.cci
}
fn account_id(&self) -> nssa::AccountId {
nssa::AccountId::from(&self.value.0.nullifier_public_key)
}
}
#[expect(
clippy::single_char_lifetime_names,
reason = "TODO add meaningful name"
)]
impl<'a> From<&'a ChildKeysPrivate> for &'a (KeyChain, nssa::Account) {
fn from(value: &'a ChildKeysPrivate) -> Self {
&value.value
impl KeyTreeNode for ChildKeysPrivate {
fn from_seed(seed: [u8; 64]) -> Self {
Self::root(seed)
}
}
#[expect(
clippy::single_char_lifetime_names,
reason = "TODO add meaningful name"
)]
impl<'a> From<&'a mut ChildKeysPrivate> for &'a mut (KeyChain, nssa::Account) {
fn from(value: &'a mut ChildKeysPrivate) -> Self {
&mut value.value
fn derive_child(&self, cci: u32) -> Self {
self.nth_child(cci)
}
fn account_ids(&self) -> impl Iterator<Item = nssa::AccountId> {
self.value.1.iter().map(|(identifier, _)| {
nssa::AccountId::from((&self.value.0.nullifier_public_key, *identifier))
})
}
}

View File

@ -1,7 +1,7 @@
use k256::elliptic_curve::{PrimeField as _, sec1::ToEncodedPoint as _};
use serde::{Deserialize, Serialize};
use crate::key_management::key_tree::traits::KeyNode;
use crate::key_management::key_tree::traits::KeyTreeNode;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ChildKeysPublic {
@ -13,32 +13,8 @@ pub struct ChildKeysPublic {
}
impl ChildKeysPublic {
fn compute_hash_value(&self, cci: u32) -> [u8; 64] {
let mut hash_input = vec![];
if ((2_u32).pow(31)).cmp(&cci) == std::cmp::Ordering::Greater {
// Non-harden.
// BIP-032 compatibility requires 1-byte header from the public_key;
// Not stored in `self.cpk.value()`.
let sk = k256::SecretKey::from_bytes(self.csk.value().into())
.expect("32 bytes, within curve order");
let pk = sk.public_key();
hash_input.extend_from_slice(pk.to_encoded_point(true).as_bytes());
} else {
// Harden.
hash_input.extend_from_slice(&[0_u8]);
hash_input.extend_from_slice(self.csk.value());
}
#[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")]
hash_input.extend_from_slice(&cci.to_be_bytes());
hmac_sha512::HMAC::mac(hash_input, self.ccc)
}
}
impl KeyNode for ChildKeysPublic {
fn root(seed: [u8; 64]) -> Self {
#[must_use]
pub fn root(seed: [u8; 64]) -> Self {
let hash_value = hmac_sha512::HMAC::mac(seed, "LEE_master_pub");
let csk = nssa::PrivateKey::try_new(
@ -58,7 +34,8 @@ impl KeyNode for ChildKeysPublic {
}
}
fn nth_child(&self, cci: u32) -> Self {
#[must_use]
pub fn nth_child(&self, cci: u32) -> Self {
let hash_value = self.compute_hash_value(cci);
let csk = nssa::PrivateKey::try_new({
@ -90,17 +67,33 @@ impl KeyNode for ChildKeysPublic {
}
}
fn chain_code(&self) -> &[u8; 32] {
&self.ccc
}
fn child_index(&self) -> Option<u32> {
self.cci
}
fn account_id(&self) -> nssa::AccountId {
#[must_use]
pub fn account_id(&self) -> nssa::AccountId {
nssa::AccountId::from(&self.cpk)
}
fn compute_hash_value(&self, cci: u32) -> [u8; 64] {
let mut hash_input = vec![];
if ((2_u32).pow(31)).cmp(&cci) == std::cmp::Ordering::Greater {
// Non-harden.
// BIP-032 compatibility requires 1-byte header from the public_key;
// Not stored in `self.cpk.value()`.
let sk = k256::SecretKey::from_bytes(self.csk.value().into())
.expect("32 bytes, within curve order");
let pk = sk.public_key();
hash_input.extend_from_slice(pk.to_encoded_point(true).as_bytes());
} else {
// Harden.
hash_input.extend_from_slice(&[0_u8]);
hash_input.extend_from_slice(self.csk.value());
}
#[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")]
hash_input.extend_from_slice(&cci.to_be_bytes());
hmac_sha512::HMAC::mac(hash_input, self.ccc)
}
}
#[expect(
@ -113,6 +106,20 @@ impl<'a> From<&'a ChildKeysPublic> for &'a nssa::PrivateKey {
}
}
impl KeyTreeNode for ChildKeysPublic {
fn from_seed(seed: [u8; 64]) -> Self {
Self::root(seed)
}
fn derive_child(&self, cci: u32) -> Self {
self.nth_child(cci)
}
fn account_ids(&self) -> impl Iterator<Item = nssa::AccountId> {
std::iter::once(self.account_id())
}
}
#[cfg(test)]
mod tests {
use nssa::{PrivateKey, PublicKey};

View File

@ -2,12 +2,13 @@ use std::collections::BTreeMap;
use anyhow::Result;
use nssa::{Account, AccountId};
use nssa_core::Identifier;
use serde::{Deserialize, Serialize};
use crate::key_management::{
key_tree::{
chain_index::ChainIndex, keys_private::ChildKeysPrivate, keys_public::ChildKeysPublic,
traits::KeyNode,
traits::KeyTreeNode,
},
secret_holders::SeedHolder,
};
@ -20,7 +21,7 @@ pub mod traits;
pub const DEPTH_SOFT_CAP: u32 = 20;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct KeyTree<N: KeyNode> {
pub struct KeyTree<N: KeyTreeNode> {
pub key_map: BTreeMap<ChainIndex, N>,
pub account_id_map: BTreeMap<nssa::AccountId, ChainIndex>,
}
@ -28,7 +29,7 @@ pub struct KeyTree<N: KeyNode> {
pub type KeyTreePublic = KeyTree<ChildKeysPublic>;
pub type KeyTreePrivate = KeyTree<ChildKeysPrivate>;
impl<N: KeyNode> KeyTree<N> {
impl<N: KeyTreeNode> KeyTree<N> {
#[must_use]
pub fn new(seed: &SeedHolder) -> Self {
let seed_fit: [u8; 64] = seed
@ -37,29 +38,62 @@ impl<N: KeyNode> KeyTree<N> {
.try_into()
.expect("SeedHolder seed is 64 bytes long");
let root_keys = N::root(seed_fit);
let account_id = root_keys.account_id();
let key_map = BTreeMap::from_iter([(ChainIndex::root(), root_keys)]);
let account_id_map = BTreeMap::from_iter([(account_id, ChainIndex::root())]);
let root_keys = N::from_seed(seed_fit);
let account_id_map = root_keys
.account_ids()
.map(|id| (id, ChainIndex::root()))
.collect();
Self {
key_map,
key_map: BTreeMap::from_iter([(ChainIndex::root(), root_keys)]),
account_id_map,
}
}
pub fn new_from_root(root: N) -> Self {
let account_id_map = BTreeMap::from_iter([(root.account_id(), ChainIndex::root())]);
let key_map = BTreeMap::from_iter([(ChainIndex::root(), root)]);
let account_id_map = root
.account_ids()
.map(|id| (id, ChainIndex::root()))
.collect();
Self {
key_map,
key_map: BTreeMap::from_iter([(ChainIndex::root(), root)]),
account_id_map,
}
}
// ToDo: Add function to create a tree from list of nodes with consistency check.
pub fn generate_new_node(&mut self, parent_cci: &ChainIndex) -> Option<ChainIndex> {
let parent_keys = self.key_map.get(parent_cci)?;
let next_child_id = self
.find_next_last_child_of_id(parent_cci)
.expect("Can be None only if parent is not present");
let next_cci = parent_cci.nth_child(next_child_id);
let child_keys = parent_keys.derive_child(next_child_id);
let account_ids = child_keys.account_ids();
for account_id in account_ids {
self.account_id_map.insert(account_id, next_cci.clone());
}
self.key_map.insert(next_cci.clone(), child_keys);
Some(next_cci)
}
pub fn fill_node(&mut self, chain_index: &ChainIndex) -> Option<ChainIndex> {
let parent_keys = self.key_map.get(&chain_index.parent()?)?;
let child_id = *chain_index.chain().last()?;
let child_keys = parent_keys.derive_child(child_id);
let account_ids = child_keys.account_ids();
for account_id in account_ids {
self.account_id_map.insert(account_id, chain_index.clone());
}
self.key_map.insert(chain_index.clone(), child_keys);
Some(chain_index.clone())
}
#[must_use]
pub fn find_next_last_child_of_id(&self, parent_id: &ChainIndex) -> Option<u32> {
@ -102,25 +136,6 @@ impl<N: KeyNode> KeyTree<N> {
}
}
pub fn generate_new_node(
&mut self,
parent_cci: &ChainIndex,
) -> Option<(nssa::AccountId, ChainIndex)> {
let parent_keys = self.key_map.get(parent_cci)?;
let next_child_id = self
.find_next_last_child_of_id(parent_cci)
.expect("Can be None only if parent is not present");
let next_cci = parent_cci.nth_child(next_child_id);
let child_keys = parent_keys.nth_child(next_child_id);
let account_id = child_keys.account_id();
self.key_map.insert(next_cci.clone(), child_keys);
self.account_id_map.insert(account_id, next_cci.clone());
Some((account_id, next_cci))
}
fn find_next_slot_layered(&self) -> ChainIndex {
let mut depth = 1;
@ -134,44 +149,10 @@ impl<N: KeyNode> KeyTree<N> {
}
}
pub fn fill_node(&mut self, chain_index: &ChainIndex) -> Option<(nssa::AccountId, ChainIndex)> {
let parent_keys = self.key_map.get(&chain_index.parent()?)?;
let child_id = *chain_index.chain().last()?;
let child_keys = parent_keys.nth_child(child_id);
let account_id = child_keys.account_id();
self.key_map.insert(chain_index.clone(), child_keys);
self.account_id_map.insert(account_id, chain_index.clone());
Some((account_id, chain_index.clone()))
}
pub fn generate_new_node_layered(&mut self) -> Option<(nssa::AccountId, ChainIndex)> {
pub fn generate_new_node_layered(&mut self) -> Option<ChainIndex> {
self.fill_node(&self.find_next_slot_layered())
}
#[must_use]
pub fn get_node(&self, account_id: nssa::AccountId) -> Option<&N> {
let chain_id = self.account_id_map.get(&account_id)?;
self.key_map.get(chain_id)
}
pub fn get_node_mut(&mut self, account_id: nssa::AccountId) -> Option<&mut N> {
let chain_id = self.account_id_map.get(&account_id)?;
self.key_map.get_mut(chain_id)
}
pub fn insert(&mut self, account_id: nssa::AccountId, chain_index: ChainIndex, node: N) {
self.account_id_map.insert(account_id, chain_index.clone());
self.key_map.insert(chain_index, node);
}
pub fn remove(&mut self, addr: nssa::AccountId) -> Option<N> {
let chain_index = self.account_id_map.remove(&addr)?;
self.key_map.remove(&chain_index)
}
/// Populates tree with children.
///
/// For given `depth` adds children to a tree such that their `ChainIndex::depth(&self) <
@ -194,37 +175,50 @@ impl<N: KeyNode> KeyTree<N> {
}
}
}
}
impl KeyTree<ChildKeysPrivate> {
/// Cleanup of non-initialized accounts in a private tree.
///
/// If account is default, removes them, stops at first non-default account.
///
/// Walks through tree in lairs of same depth using `ChainIndex::chain_ids_at_depth()`.
///
/// Chain must be parsed for accounts beforehand.
///
/// Slow, maintains tree consistency.
pub fn cleanup_tree_remove_uninit_layered(&mut self, depth: u32) {
let depth = usize::try_from(depth).expect("Depth is expected to fit in usize");
'outer: for i in (1..depth).rev() {
println!("Cleanup of tree at depth {i}");
for id in ChainIndex::chain_ids_at_depth(i) {
if let Some(node) = self.key_map.get(&id) {
if node.value.1 == nssa::Account::default() {
let addr = node.account_id();
self.remove(addr);
} else {
break 'outer;
}
}
}
}
#[must_use]
pub fn get_node(&self, account_id: nssa::AccountId) -> Option<&N> {
let chain_id = self.account_id_map.get(&account_id)?;
self.key_map.get(chain_id)
}
pub fn get_node_mut(&mut self, account_id: nssa::AccountId) -> Option<&mut N> {
let chain_id = self.account_id_map.get(&account_id)?;
self.key_map.get_mut(chain_id)
}
pub fn insert(&mut self, account_id: nssa::AccountId, chain_index: ChainIndex, node: N) {
self.account_id_map.insert(account_id, chain_index.clone());
self.key_map.insert(chain_index, node);
}
pub fn remove(&mut self, addr: nssa::AccountId) -> Option<N> {
let chain_index = self.account_id_map.remove(&addr)?;
self.key_map.remove(&chain_index)
}
}
impl KeyTree<ChildKeysPublic> {
/// Generate a new public key node, returning the account ID and chain index.
pub fn generate_new_public_node(
&mut self,
parent_cci: &ChainIndex,
) -> Option<(nssa::AccountId, ChainIndex)> {
let cci = self.generate_new_node(parent_cci)?;
let node = self.key_map.get(&cci)?;
let account_id = node.account_ids().next()?;
Some((account_id, cci))
}
/// Generate a new public key node using layered placement, returning the account ID and chain
/// index.
pub fn generate_new_public_node_layered(&mut self) -> Option<(nssa::AccountId, ChainIndex)> {
let cci = self.generate_new_node_layered()?;
let node = self.key_map.get(&cci)?;
let account_id = node.account_ids().next()?;
Some((account_id, cci))
}
/// Cleanup of non-initialized accounts in a public tree.
///
/// If account is default, removes them, stops at first non-default account.
@ -259,6 +253,65 @@ impl KeyTree<ChildKeysPublic> {
}
}
impl KeyTree<ChildKeysPrivate> {
pub fn create_private_accounts_key_node(
&mut self,
parent_cci: &ChainIndex,
) -> Option<ChainIndex> {
self.generate_new_node(parent_cci)
}
pub fn create_private_accounts_key_node_layered(&mut self) -> Option<ChainIndex> {
self.generate_new_node_layered()
}
/// Register an additional identifier on an existing private key node, inserting the derived
/// `AccountId` into `account_id_map`. Returns `None` if the node does not exist or the
/// `AccountId` is already registered.
pub fn register_identifier_on_node(
&mut self,
cci: &ChainIndex,
identifier: Identifier,
) -> Option<nssa::AccountId> {
let node = self.key_map.get(cci)?;
let account_id = nssa::AccountId::from((&node.value.0.nullifier_public_key, identifier));
if self.account_id_map.contains_key(&account_id) {
return None;
}
self.account_id_map.insert(account_id, cci.clone());
Some(account_id)
}
/// Cleanup of non-initialized accounts in a private tree.
///
/// If account has no synced entries, removes it, stops at first initialized account.
///
/// Walks through tree in layers of same depth using `ChainIndex::chain_ids_at_depth()`.
///
/// Chain must be parsed for accounts beforehand.
///
/// Slow, maintains tree consistency.
pub fn cleanup_tree_remove_uninit_layered(&mut self, depth: u32) {
let depth = usize::try_from(depth).expect("Depth is expected to fit in usize");
'outer: for i in (1..depth).rev() {
println!("Cleanup of tree at depth {i}");
for id in ChainIndex::chain_ids_at_depth(i) {
if let Some(node) = self.key_map.get(&id).cloned() {
if node.value.1.is_empty() {
let account_ids = node.account_ids();
self.key_map.remove(&id);
for addr in account_ids {
self.account_id_map.remove(&addr);
}
} else {
break 'outer;
}
}
}
}
}
}
#[cfg(test)]
mod tests {
#![expect(clippy::shadow_unrelated, reason = "We don't care about this in tests")]
@ -478,25 +531,59 @@ mod tests {
.key_map
.get_mut(&ChainIndex::from_str("/1").unwrap())
.unwrap();
acc.value.1.balance = 2;
acc.value.1.push((
0,
nssa::Account {
balance: 2,
..nssa::Account::default()
},
));
let acc = tree
.key_map
.get_mut(&ChainIndex::from_str("/2").unwrap())
.unwrap();
acc.value.1.balance = 3;
acc.value.1.push((
0,
nssa::Account {
balance: 3,
..nssa::Account::default()
},
));
let acc = tree
.key_map
.get_mut(&ChainIndex::from_str("/0/1").unwrap())
.unwrap();
acc.value.1.balance = 5;
acc.value.1.push((
0,
nssa::Account {
balance: 5,
..nssa::Account::default()
},
));
let acc = tree
.key_map
.get_mut(&ChainIndex::from_str("/1/0").unwrap())
.unwrap();
acc.value.1.balance = 6;
acc.value.1.push((
0,
nssa::Account {
balance: 6,
..nssa::Account::default()
},
));
// Update account_id_map for nodes that now have entries
for chain_index_str in ["/1", "/2", "/0/1", "/1/0"] {
let id = ChainIndex::from_str(chain_index_str).unwrap();
if let Some(node) = tree.key_map.get(&id) {
for account_id in node.account_ids() {
tree.account_id_map.insert(account_id, id.clone());
}
}
}
tree.cleanup_tree_remove_uninit_layered(10);
@ -518,15 +605,15 @@ mod tests {
assert_eq!(key_set, key_set_res);
let acc = &tree.key_map[&ChainIndex::from_str("/1").unwrap()];
assert_eq!(acc.value.1.balance, 2);
assert_eq!(acc.value.1[0].1.balance, 2);
let acc = &tree.key_map[&ChainIndex::from_str("/2").unwrap()];
assert_eq!(acc.value.1.balance, 3);
assert_eq!(acc.value.1[0].1.balance, 3);
let acc = &tree.key_map[&ChainIndex::from_str("/0/1").unwrap()];
assert_eq!(acc.value.1.balance, 5);
assert_eq!(acc.value.1[0].1.balance, 5);
let acc = &tree.key_map[&ChainIndex::from_str("/1/0").unwrap()];
assert_eq!(acc.value.1.balance, 6);
assert_eq!(acc.value.1[0].1.balance, 6);
}
}

View File

@ -1,15 +1,8 @@
/// Trait, that reperesents a Node in hierarchical key tree.
pub trait KeyNode {
/// Tree root node.
fn root(seed: [u8; 64]) -> Self;
/// `cci`'s child of node.
pub trait KeyTreeNode: Sized {
#[must_use]
fn nth_child(&self, cci: u32) -> Self;
fn chain_code(&self) -> &[u8; 32];
fn child_index(&self) -> Option<u32>;
fn account_id(&self) -> nssa::AccountId;
fn from_seed(seed: [u8; 64]) -> Self;
#[must_use]
fn derive_child(&self, cci: u32) -> Self;
#[must_use]
fn account_ids(&self) -> impl Iterator<Item = nssa::AccountId>;
}

View File

@ -173,11 +173,12 @@ mod tests {
// /0/0
key_tree_private.generate_new_node_layered().unwrap();
// /2
let (second_child_id, _) = key_tree_private.generate_new_node_layered().unwrap();
let second_chain_index = key_tree_private.generate_new_node_layered().unwrap();
key_tree_private
.get_node(second_child_id)
.unwrap()
.key_map
.get(&second_chain_index)
.expect("Node was just inserted")
.value
.0
.clone()

View File

@ -2,6 +2,8 @@ use std::collections::BTreeMap;
use anyhow::Result;
use k256::AffinePoint;
use nssa::{Account, AccountId};
use nssa_core::Identifier;
use serde::{Deserialize, Serialize};
use crate::key_management::{
@ -13,13 +15,18 @@ use crate::key_management::{
pub type PublicKey = AffinePoint;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct UserPrivateAccountData {
pub key_chain: KeyChain,
pub accounts: Vec<(Identifier, Account)>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NSSAUserData {
/// Default public accounts.
pub default_pub_account_signing_keys: BTreeMap<nssa::AccountId, nssa::PrivateKey>,
/// Default private accounts.
pub default_user_private_accounts:
BTreeMap<nssa::AccountId, (KeyChain, nssa_core::account::Account)>,
pub default_user_private_accounts: BTreeMap<AccountId, UserPrivateAccountData>,
/// Tree of public keys.
pub public_key_tree: KeyTreePublic,
/// Tree of private keys.
@ -54,13 +61,16 @@ impl NSSAUserData {
}
fn valid_private_key_transaction_pairing_check(
accounts_keys_map: &BTreeMap<nssa::AccountId, (KeyChain, nssa_core::account::Account)>,
accounts_keys_map: &BTreeMap<AccountId, UserPrivateAccountData>,
) -> bool {
let mut check_res = true;
for (account_id, (key, _)) in accounts_keys_map {
let expected_account_id = nssa::AccountId::from(&key.nullifier_public_key);
if expected_account_id != *account_id {
println!("{expected_account_id}, {account_id}");
for (account_id, entry) in accounts_keys_map {
let any_match = entry.accounts.iter().any(|(identifier, _)| {
nssa::AccountId::from((&entry.key_chain.nullifier_public_key, *identifier))
== *account_id
});
if !any_match {
println!("No matching entry found for account_id {account_id}");
check_res = false;
}
}
@ -69,10 +79,7 @@ impl NSSAUserData {
pub fn new_with_accounts(
default_accounts_keys: BTreeMap<nssa::AccountId, nssa::PrivateKey>,
default_accounts_key_chains: BTreeMap<
nssa::AccountId,
(KeyChain, nssa_core::account::Account),
>,
default_accounts_key_chains: BTreeMap<AccountId, UserPrivateAccountData>,
public_key_tree: KeyTreePublic,
private_key_tree: KeyTreePrivate,
) -> Result<Self> {
@ -108,11 +115,11 @@ impl NSSAUserData {
match parent_cci {
Some(parent_cci) => self
.public_key_tree
.generate_new_node(&parent_cci)
.generate_new_public_node(&parent_cci)
.expect("Parent must be present in a tree"),
None => self
.public_key_tree
.generate_new_node_layered()
.generate_new_public_node_layered()
.expect("Search for new node slot failed"),
}
}
@ -128,50 +135,61 @@ impl NSSAUserData {
.or_else(|| self.public_key_tree.get_node(account_id).map(Into::into))
}
/// Generated new private key for privacy preserving transactions.
///
/// Returns the `account_id` of new account.
pub fn generate_new_privacy_preserving_transaction_key_chain(
&mut self,
parent_cci: Option<ChainIndex>,
) -> (nssa::AccountId, ChainIndex) {
/// Creates a new receiving key node and returns its `ChainIndex`.
pub fn create_private_accounts_key(&mut self, parent_cci: Option<ChainIndex>) -> ChainIndex {
match parent_cci {
Some(parent_cci) => self
.private_key_tree
.generate_new_node(&parent_cci)
.create_private_accounts_key_node(&parent_cci)
.expect("Parent must be present in a tree"),
None => self
.private_key_tree
.generate_new_node_layered()
.create_private_accounts_key_node_layered()
.expect("Search for new node slot failed"),
}
}
/// Returns the signing key for public transaction signatures.
/// Registers an additional identifier on an existing private key node, deriving and recording
/// the corresponding `AccountId`. Returns `None` if the node does not exist or the identifier
/// is already registered.
pub fn register_identifier_on_private_key_chain(
&mut self,
cci: &ChainIndex,
identifier: Identifier,
) -> Option<nssa::AccountId> {
self.private_key_tree
.register_identifier_on_node(cci, identifier)
}
/// Returns the key chain and account data for the given private account ID.
#[must_use]
pub fn get_private_account(
&self,
account_id: nssa::AccountId,
) -> Option<&(KeyChain, nssa_core::account::Account)> {
self.default_user_private_accounts
.get(&account_id)
.or_else(|| self.private_key_tree.get_node(account_id).map(Into::into))
}
/// Returns the signing key for public transaction signatures.
pub fn get_private_account_mut(
&mut self,
account_id: &nssa::AccountId,
) -> Option<&mut (KeyChain, nssa_core::account::Account)> {
// First seek in defaults
if let Some(key) = self.default_user_private_accounts.get_mut(account_id) {
Some(key)
// Then seek in tree
} else {
self.private_key_tree
.get_node_mut(*account_id)
.map(Into::into)
) -> Option<(KeyChain, nssa_core::account::Account, Identifier)> {
// Check default accounts
if let Some(entry) = self.default_user_private_accounts.get(&account_id) {
for (identifier, account) in &entry.accounts {
let expected_id =
nssa::AccountId::from((&entry.key_chain.nullifier_public_key, *identifier));
if expected_id == account_id {
return Some((entry.key_chain.clone(), account.clone(), *identifier));
}
}
return None;
}
// Check tree
if let Some(node) = self.private_key_tree.get_node(account_id) {
let key_chain = &node.value.0;
for (identifier, account) in &node.value.1 {
let expected_id =
nssa::AccountId::from((&key_chain.nullifier_public_key, *identifier));
if expected_id == account_id {
return Some((key_chain.clone(), account.clone(), *identifier));
}
}
}
None
}
pub fn account_ids(&self) -> impl Iterator<Item = nssa::AccountId> {
@ -249,16 +267,15 @@ mod tests {
fn new_account() {
let mut user_data = NSSAUserData::default();
let (account_id_private, _) = user_data
.generate_new_privacy_preserving_transaction_key_chain(Some(ChainIndex::root()));
let is_key_chain_generated = user_data.get_private_account(account_id_private).is_some();
let chain_index = user_data.create_private_accounts_key(Some(ChainIndex::root()));
let is_key_chain_generated = user_data
.private_key_tree
.key_map
.contains_key(&chain_index);
assert!(is_key_chain_generated);
let account_id_private_str = account_id_private.to_string();
println!("{account_id_private_str:#?}");
let key_chain = &user_data.get_private_account(account_id_private).unwrap().0;
let key_chain = &user_data.private_key_tree.key_map[&chain_index].value.0;
println!("{key_chain:#?}");
}
}

View File

@ -10,7 +10,7 @@ use risc0_zkvm::sha::{Impl, Sha256 as _};
use serde::{Deserialize, Serialize};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use crate::{NullifierPublicKey, NullifierSecretKey, program::ProgramId};
use crate::{NullifierSecretKey, program::ProgramId};
pub mod data;
@ -26,9 +26,9 @@ impl Nonce {
}
#[must_use]
pub fn private_account_nonce_init(npk: &NullifierPublicKey) -> Self {
pub fn private_account_nonce_init(account_id: &AccountId) -> Self {
let mut bytes: [u8; 64] = [0_u8; 64];
bytes[..32].copy_from_slice(&npk.0);
bytes[..32].copy_from_slice(account_id.value());
let result: [u8; 32] = Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap();
let result = result.first_chunk::<16>().unwrap();
@ -306,8 +306,8 @@ mod tests {
#[test]
fn initialize_private_nonce() {
let npk = NullifierPublicKey([42; 32]);
let nonce = Nonce::private_account_nonce_init(&npk);
let account_id = AccountId::new([42; 32]);
let nonce = Nonce::private_account_nonce_init(&account_id);
let expected_nonce = Nonce(37_937_661_125_547_691_021_612_781_941_709_513_486);
assert_eq!(nonce, expected_nonce);
}

View File

@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize};
use crate::{
Commitment, CommitmentSetDigest, MembershipProof, Nullifier, NullifierPublicKey,
Commitment, CommitmentSetDigest, Identifier, MembershipProof, Nullifier, NullifierPublicKey,
NullifierSecretKey, SharedSecretKey,
account::{Account, AccountWithMetadata},
encryption::Ciphertext,
@ -19,8 +19,8 @@ pub struct PrivacyPreservingCircuitInput {
/// - `2` - private account without authentication
/// - `3` - private PDA account
pub visibility_mask: Vec<u8>,
/// Public keys of private accounts.
pub private_account_keys: Vec<(NullifierPublicKey, SharedSecretKey)>,
/// Public keys and identifiers of private accounts.
pub private_account_keys: Vec<(NullifierPublicKey, Identifier, SharedSecretKey)>,
/// Nullifier secret keys for authorized private accounts.
pub private_account_nsks: Vec<NullifierSecretKey>,
/// Membership proofs for private accounts. Can be [`None`] for uninitialized accounts.
@ -57,7 +57,7 @@ mod tests {
use super::*;
use crate::{
Commitment, Nullifier, NullifierPublicKey,
Commitment, Nullifier,
account::{Account, AccountId, AccountWithMetadata, Nonce},
};
@ -94,12 +94,12 @@ mod tests {
}],
ciphertexts: vec![Ciphertext(vec![255, 255, 1, 1, 2, 2])],
new_commitments: vec![Commitment::new(
&NullifierPublicKey::from(&[1; 32]),
&AccountId::new([1; 32]),
&Account::default(),
)],
new_nullifiers: vec![(
Nullifier::for_account_update(
&Commitment::new(&NullifierPublicKey::from(&[2; 32]), &Account::default()),
&Commitment::new(&AccountId::new([2; 32]), &Account::default()),
&[1; 32],
),
[0xab; 32],

View File

@ -2,7 +2,7 @@ use borsh::{BorshDeserialize, BorshSerialize};
use risc0_zkvm::sha::{Impl, Sha256 as _};
use serde::{Deserialize, Serialize};
use crate::{NullifierPublicKey, account::Account};
use crate::account::{Account, AccountId};
/// A commitment to all zero data.
/// ```python
@ -49,16 +49,16 @@ impl std::fmt::Debug for Commitment {
}
impl Commitment {
/// Generates the commitment to a private account owned by user for npk:
/// SHA256( `Comm_DS` || npk || `program_owner` || balance || nonce || SHA256(data)).
/// Generates the commitment to a private account owned by user for `account_id`:
/// SHA256( `Comm_DS` || `account_id` || `program_owner` || balance || nonce || SHA256(data)).
#[must_use]
pub fn new(npk: &NullifierPublicKey, account: &Account) -> Self {
pub fn new(account_id: &AccountId, account: &Account) -> Self {
const COMMITMENT_PREFIX: &[u8; 32] =
b"/LEE/v0.3/Commitment/\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
let mut bytes = Vec::new();
bytes.extend_from_slice(COMMITMENT_PREFIX);
bytes.extend_from_slice(&npk.to_byte_array());
bytes.extend_from_slice(account_id.value());
let account_bytes_with_hashed_data = {
let mut this = Vec::new();
for word in &account.program_owner {
@ -115,14 +115,15 @@ mod tests {
use risc0_zkvm::sha::{Impl, Sha256 as _};
use crate::{
Commitment, DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, NullifierPublicKey, account::Account,
Commitment, DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH,
account::{Account, AccountId},
};
#[test]
fn nothing_up_my_sleeve_dummy_commitment() {
let default_account = Account::default();
let npk_null = NullifierPublicKey([0; 32]);
let expected_dummy_commitment = Commitment::new(&npk_null, &default_account);
let account_id_null = AccountId::new([0; 32]);
let expected_dummy_commitment = Commitment::new(&account_id_null, &default_account);
assert_eq!(DUMMY_COMMITMENT, expected_dummy_commitment);
}

View File

@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
#[cfg(feature = "host")]
pub use shared_key_derivation::{EphemeralPublicKey, EphemeralSecretKey, ViewingPublicKey};
use crate::{Commitment, account::Account};
use crate::{Commitment, Identifier, account::Account};
#[cfg(feature = "host")]
pub mod shared_key_derivation;
@ -40,11 +40,14 @@ impl EncryptionScheme {
#[must_use]
pub fn encrypt(
account: &Account,
identifier: Identifier,
shared_secret: &SharedSecretKey,
commitment: &Commitment,
output_index: u32,
) -> Ciphertext {
let mut buffer = account.to_bytes();
// Plaintext: identifier (16 bytes, little-endian) || account bytes
let mut buffer = identifier.to_le_bytes().to_vec();
buffer.extend_from_slice(&account.to_bytes());
Self::symmetric_transform(&mut buffer, shared_secret, commitment, output_index);
Ciphertext(buffer)
}
@ -86,12 +89,17 @@ impl EncryptionScheme {
shared_secret: &SharedSecretKey,
commitment: &Commitment,
output_index: u32,
) -> Option<Account> {
) -> Option<(Identifier, Account)> {
use std::io::Cursor;
let mut buffer = ciphertext.0.clone();
Self::symmetric_transform(&mut buffer, shared_secret, commitment, output_index);
let mut cursor = Cursor::new(buffer.as_slice());
if buffer.len() < 16 {
return None;
}
let identifier = Identifier::from_le_bytes(buffer[..16].try_into().unwrap());
let mut cursor = Cursor::new(&buffer[16..]);
Account::from_cursor(&mut cursor)
.inspect_err(|err| {
println!(
@ -104,5 +112,6 @@ impl EncryptionScheme {
);
})
.ok()
.map(|account| (identifier, account))
}
}

View File

@ -9,7 +9,7 @@ pub use commitment::{
compute_digest_for_path,
};
pub use encryption::{EncryptionScheme, SharedSecretKey};
pub use nullifier::{Nullifier, NullifierPublicKey, NullifierSecretKey};
pub use nullifier::{Identifier, Nullifier, NullifierPublicKey, NullifierSecretKey};
pub mod account;
mod circuit_io;

View File

@ -4,18 +4,24 @@ use serde::{Deserialize, Serialize};
use crate::{Commitment, account::AccountId};
const PRIVATE_ACCOUNT_ID_PREFIX: &[u8; 32] = b"/LEE/v0.3/AccountId/Private/\x00\x00\x00\x00";
pub type Identifier = u128;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(any(feature = "host", test), derive(Hash))]
pub struct NullifierPublicKey(pub [u8; 32]);
impl From<&NullifierPublicKey> for AccountId {
fn from(value: &NullifierPublicKey) -> Self {
const PRIVATE_ACCOUNT_ID_PREFIX: &[u8; 32] =
b"/LEE/v0.3/AccountId/Private/\x00\x00\x00\x00";
impl From<(&NullifierPublicKey, Identifier)> for AccountId {
fn from(value: (&NullifierPublicKey, Identifier)) -> Self {
let (npk, identifier) = value;
let mut bytes = [0; 64];
// 32 bytes prefix || 32 bytes npk || 16 bytes identifier
let mut bytes = [0; 80];
bytes[0..32].copy_from_slice(PRIVATE_ACCOUNT_ID_PREFIX);
bytes[32..].copy_from_slice(&value.0);
bytes[32..64].copy_from_slice(&npk.0);
bytes[64..80].copy_from_slice(&identifier.to_le_bytes());
Self::new(
Impl::hash_bytes(&bytes)
.as_bytes()
@ -85,10 +91,10 @@ impl Nullifier {
/// Computes a nullifier for an account initialization.
#[must_use]
pub fn for_account_initialization(npk: &NullifierPublicKey) -> Self {
pub fn for_account_initialization(account_id: &AccountId) -> Self {
const INIT_PREFIX: &[u8; 32] = b"/LEE/v0.3/Nullifier/Initialize/\x00";
let mut bytes = INIT_PREFIX.to_vec();
bytes.extend_from_slice(&npk.to_byte_array());
bytes.extend_from_slice(account_id.value());
Self(Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap())
}
}
@ -111,7 +117,7 @@ mod tests {
#[test]
fn constructor_for_account_initialization() {
let npk = NullifierPublicKey([
let account_id = AccountId::new([
112, 188, 193, 129, 150, 55, 228, 67, 88, 168, 29, 151, 5, 92, 23, 190, 17, 162, 164,
255, 29, 105, 42, 186, 43, 11, 157, 168, 132, 225, 17, 163,
]);
@ -119,7 +125,7 @@ mod tests {
149, 59, 95, 181, 2, 194, 20, 143, 72, 233, 104, 243, 59, 70, 67, 243, 110, 77, 109,
132, 139, 111, 51, 125, 128, 92, 107, 46, 252, 4, 20, 149,
]);
let nullifier = Nullifier::for_account_initialization(&npk);
let nullifier = Nullifier::for_account_initialization(&account_id);
assert_eq!(nullifier, expected_nullifier);
}
@ -145,11 +151,46 @@ mod tests {
];
let npk = NullifierPublicKey::from(&nsk);
let expected_account_id = AccountId::new([
139, 72, 194, 222, 215, 187, 147, 56, 55, 35, 222, 205, 156, 12, 204, 227, 166, 44, 30,
81, 186, 14, 167, 234, 28, 236, 32, 213, 125, 251, 193, 233,
165, 52, 40, 32, 231, 171, 113, 10, 65, 241, 156, 72, 154, 207, 122, 192, 15, 46, 50,
253, 105, 164, 89, 84, 40, 191, 182, 119, 64, 255, 67, 142,
]);
let account_id = AccountId::from(&npk);
let account_id = AccountId::from((&npk, 0));
assert_eq!(account_id, expected_account_id);
}
#[test]
fn account_id_from_nullifier_public_key_identifier_1() {
let nsk = [
57, 5, 64, 115, 153, 56, 184, 51, 207, 238, 99, 165, 147, 214, 213, 151, 30, 251, 30,
196, 134, 22, 224, 211, 237, 120, 136, 225, 188, 220, 249, 28,
];
let npk = NullifierPublicKey::from(&nsk);
let expected_account_id = AccountId::new([
203, 201, 109, 245, 40, 54, 195, 12, 55, 33, 0, 86, 245, 65, 70, 156, 24, 249, 26, 95,
56, 247, 99, 121, 165, 182, 234, 255, 19, 127, 191, 72,
]);
let account_id = AccountId::from((&npk, 1));
assert_eq!(account_id, expected_account_id);
}
#[test]
fn account_id_from_nullifier_public_key_byte_asymmetric_identifier() {
let identifier: u128 = 0x0123_4567_89AB_CDEF_FEDC_BA98_7654_3210;
let nsk = [
57, 5, 64, 115, 153, 56, 184, 51, 207, 238, 99, 165, 147, 214, 213, 151, 30, 251, 30,
196, 134, 22, 224, 211, 237, 120, 136, 225, 188, 220, 249, 28,
];
let npk = NullifierPublicKey::from(&nsk);
let expected_account_id = AccountId::new([
178, 16, 226, 206, 217, 38, 38, 45, 155, 240, 226, 253, 168, 87, 146, 70, 72, 32, 174,
19, 245, 25, 214, 162, 209, 135, 252, 82, 27, 2, 174, 196,
]);
let account_id = AccountId::from((&npk, identifier));
assert_eq!(account_id, expected_account_id);
}

View File

@ -919,18 +919,6 @@ mod tests {
assert_ne!(private_id, public_id);
}
/// A private PDA address differs from a standard private account address at the same `npk`,
/// because the private PDA formula includes `program_id` and `seed`.
#[test]
fn for_private_pda_differs_from_standard_private() {
let program_id: ProgramId = [1; 8];
let seed = PdaSeed::new([2; 32]);
let npk = NullifierPublicKey([3; 32]);
let private_pda_id = AccountId::for_private_pda(&program_id, &seed, &npk);
let standard_private_id = AccountId::from(&npk);
assert_ne!(private_pda_id, standard_private_id);
}
// ---- compute_public_authorized_pdas tests ----
/// `compute_public_authorized_pdas` returns the public PDA addresses for the caller's seeds.

View File

@ -2,8 +2,8 @@ use std::collections::{HashMap, VecDeque};
use borsh::{BorshDeserialize, BorshSerialize};
use nssa_core::{
MembershipProof, NullifierPublicKey, NullifierSecretKey, PrivacyPreservingCircuitInput,
PrivacyPreservingCircuitOutput, SharedSecretKey,
Identifier, MembershipProof, NullifierPublicKey, NullifierSecretKey,
PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, SharedSecretKey,
account::AccountWithMetadata,
program::{ChainedCall, InstructionData, ProgramId, ProgramOutput},
};
@ -68,7 +68,7 @@ pub fn execute_and_prove(
pre_states: Vec<AccountWithMetadata>,
instruction_data: InstructionData,
visibility_mask: Vec<u8>,
private_account_keys: Vec<(NullifierPublicKey, SharedSecretKey)>,
private_account_keys: Vec<(NullifierPublicKey, Identifier, SharedSecretKey)>,
private_account_nsks: Vec<NullifierSecretKey>,
private_account_membership_proofs: Vec<Option<MembershipProof>>,
program_with_dependencies: &ProgramWithDependencies,
@ -214,11 +214,8 @@ mod tests {
AccountId::new([0; 32]),
);
let recipient = AccountWithMetadata::new(
Account::default(),
false,
AccountId::from(&recipient_keys.npk()),
);
let recipient_account_id = AccountId::from((&recipient_keys.npk(), 0));
let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id);
let balance_to_move: u128 = 37;
@ -232,7 +229,7 @@ mod tests {
let expected_recipient_post = Account {
program_owner: program.id(),
balance: balance_to_move,
nonce: Nonce::private_account_nonce_init(&recipient_keys.npk()),
nonce: Nonce::private_account_nonce_init(&recipient_account_id),
data: Data::default(),
};
@ -245,7 +242,7 @@ mod tests {
vec![sender, recipient],
Program::serialize_instruction(balance_to_move).unwrap(),
vec![0, 2],
vec![(recipient_keys.npk(), shared_secret)],
vec![(recipient_keys.npk(), 0, shared_secret)],
vec![],
vec![None],
&Program::authenticated_transfer_program().into(),
@ -262,7 +259,7 @@ mod tests {
assert_eq!(output.new_nullifiers.len(), 1);
assert_eq!(output.ciphertexts.len(), 1);
let recipient_post = EncryptionScheme::decrypt(
let (_identifier, recipient_post) = EncryptionScheme::decrypt(
&output.ciphertexts[0],
&shared_secret,
&output.new_commitments[0],
@ -287,27 +284,24 @@ mod tests {
data: Data::default(),
},
true,
AccountId::from(&sender_keys.npk()),
AccountId::from((&sender_keys.npk(), 0)),
);
let commitment_sender = Commitment::new(&sender_keys.npk(), &sender_pre.account);
let sender_account_id = AccountId::from((&sender_keys.npk(), 0));
let commitment_sender = Commitment::new(&sender_account_id, &sender_pre.account);
let recipient = AccountWithMetadata::new(
Account::default(),
false,
AccountId::from(&recipient_keys.npk()),
);
let recipient_account_id = AccountId::from((&recipient_keys.npk(), 0));
let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id);
let balance_to_move: u128 = 37;
let mut commitment_set = CommitmentSet::with_capacity(2);
commitment_set.extend(std::slice::from_ref(&commitment_sender));
let expected_new_nullifiers = vec![
(
Nullifier::for_account_update(&commitment_sender, &sender_keys.nsk),
commitment_set.digest(),
),
(
Nullifier::for_account_initialization(&recipient_keys.npk()),
Nullifier::for_account_initialization(&recipient_account_id),
DUMMY_COMMITMENT_HASH,
),
];
@ -323,12 +317,12 @@ mod tests {
let expected_private_account_2 = Account {
program_owner: program.id(),
balance: balance_to_move,
nonce: Nonce::private_account_nonce_init(&recipient_keys.npk()),
nonce: Nonce::private_account_nonce_init(&recipient_account_id),
..Default::default()
};
let expected_new_commitments = vec![
Commitment::new(&sender_keys.npk(), &expected_private_account_1),
Commitment::new(&recipient_keys.npk(), &expected_private_account_2),
Commitment::new(&sender_account_id, &expected_private_account_1),
Commitment::new(&recipient_account_id, &expected_private_account_2),
];
let esk_1 = [3; 32];
@ -342,8 +336,8 @@ mod tests {
Program::serialize_instruction(balance_to_move).unwrap(),
vec![1, 2],
vec![
(sender_keys.npk(), shared_secret_1),
(recipient_keys.npk(), shared_secret_2),
(sender_keys.npk(), 0, shared_secret_1),
(recipient_keys.npk(), 0, shared_secret_2),
],
vec![sender_keys.nsk],
vec![commitment_set.get_proof_for(&commitment_sender), None],
@ -358,7 +352,7 @@ mod tests {
assert_eq!(output.new_nullifiers, expected_new_nullifiers);
assert_eq!(output.ciphertexts.len(), 2);
let sender_post = EncryptionScheme::decrypt(
let (_identifier, sender_post) = EncryptionScheme::decrypt(
&output.ciphertexts[0],
&shared_secret_1,
&expected_new_commitments[0],
@ -367,7 +361,7 @@ mod tests {
.unwrap();
assert_eq!(sender_post, expected_private_account_1);
let recipient_post = EncryptionScheme::decrypt(
let (_identifier, recipient_post) = EncryptionScheme::decrypt(
&output.ciphertexts[1],
&shared_secret_2,
&expected_new_commitments[1],
@ -383,7 +377,7 @@ mod tests {
let pre = AccountWithMetadata::new(
Account::default(),
false,
AccountId::from(&account_keys.npk()),
AccountId::from((&account_keys.npk(), 0)),
);
let validity_window_chain_caller = Program::validity_window_chain_caller();
@ -410,7 +404,7 @@ mod tests {
vec![pre],
instruction,
vec![2],
vec![(account_keys.npk(), shared_secret)],
vec![(account_keys.npk(), 0, shared_secret)],
vec![],
vec![None],
&program_with_deps,
@ -459,7 +453,7 @@ mod tests {
vec![pda_pre, sender_pre],
instruction,
vec![3, 0],
vec![(npk, shared_secret_pda)],
vec![(npk, 0, shared_secret_pda)],
vec![],
vec![None],
&program_with_deps,
@ -505,7 +499,7 @@ mod tests {
vec![pda_pre, bob_pre],
instruction,
vec![3, 0],
vec![(npk, shared_secret_pda)],
vec![(npk, 0, shared_secret_pda)],
vec![],
vec![None],
&program_with_deps,

View File

@ -154,9 +154,11 @@ pub mod tests {
let encrypted_private_post_states = Vec::new();
let new_commitments = vec![Commitment::new(&npk2, &account2)];
let account_id2 = nssa_core::account::AccountId::from((&npk2, 0));
let new_commitments = vec![Commitment::new(&account_id2, &account2)];
let old_commitment = Commitment::new(&npk1, &account1);
let account_id1 = nssa_core::account::AccountId::from((&npk1, 0));
let old_commitment = Commitment::new(&account_id1, &account1);
let new_nullifiers = vec![(
Nullifier::for_account_update(&old_commitment, &nsk1),
[0; 32],
@ -179,11 +181,12 @@ pub mod tests {
let npk = NullifierPublicKey::from(&[1; 32]);
let vpk = ViewingPublicKey::from_scalar([2; 32]);
let account = Account::default();
let commitment = Commitment::new(&npk, &account);
let account_id = nssa_core::account::AccountId::from((&npk, 0));
let commitment = Commitment::new(&account_id, &account);
let esk = [3; 32];
let shared_secret = SharedSecretKey::new(&esk, &vpk);
let epk = EphemeralPublicKey::from_scalar(esk);
let ciphertext = EncryptionScheme::encrypt(&account, &shared_secret, &commitment, 2);
let ciphertext = EncryptionScheme::encrypt(&account, 0, &shared_secret, &commitment, 2);
let encrypted_account_data =
EncryptedAccountData::new(ciphertext.clone(), &npk, &vpk, epk.clone());

View File

@ -459,7 +459,8 @@ pub mod tests {
#[must_use]
pub fn with_private_account(mut self, keys: &TestPrivateKeys, account: &Account) -> Self {
let commitment = Commitment::new(&keys.npk(), account);
let account_id = AccountId::from((&keys.npk(), 0));
let commitment = Commitment::new(&account_id, account);
self.private_state.0.extend(&[commitment]);
self
}
@ -617,13 +618,13 @@ pub mod tests {
..Account::default()
};
let npk1 = keys1.npk();
let npk2 = keys2.npk();
let account_id1 = AccountId::from((&keys1.npk(), 0));
let account_id2 = AccountId::from((&keys2.npk(), 0));
let init_commitment1 = Commitment::new(&npk1, &account);
let init_commitment2 = Commitment::new(&npk2, &account);
let init_nullifier1 = Nullifier::for_account_initialization(&npk1);
let init_nullifier2 = Nullifier::for_account_initialization(&npk2);
let init_commitment1 = Commitment::new(&account_id1, &account);
let init_commitment2 = Commitment::new(&account_id2, &account);
let init_nullifier1 = Nullifier::for_account_initialization(&account_id1);
let init_nullifier2 = Nullifier::for_account_initialization(&account_id2);
let initial_private_accounts = vec![
(init_commitment1, init_nullifier1),
@ -1283,7 +1284,8 @@ pub mod tests {
let sender_nonce = sender.account.nonce;
let recipient = AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk());
let recipient =
AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0));
let esk = [3; 32];
let shared_secret = SharedSecretKey::new(&esk, &recipient_keys.vpk());
@ -1293,7 +1295,7 @@ pub mod tests {
vec![sender, recipient],
Program::serialize_instruction(balance_to_move).unwrap(),
vec![0, 2],
vec![(recipient_keys.npk(), shared_secret)],
vec![(recipient_keys.npk(), 0, shared_secret)],
vec![],
vec![None],
&Program::authenticated_transfer_program().into(),
@ -1320,11 +1322,15 @@ pub mod tests {
state: &V03State,
) -> PrivacyPreservingTransaction {
let program = Program::authenticated_transfer_program();
let sender_commitment = Commitment::new(&sender_keys.npk(), sender_private_account);
let sender_pre =
AccountWithMetadata::new(sender_private_account.clone(), true, &sender_keys.npk());
let sender_account_id = AccountId::from((&sender_keys.npk(), 0));
let sender_commitment = Commitment::new(&sender_account_id, sender_private_account);
let sender_pre = AccountWithMetadata::new(
sender_private_account.clone(),
true,
(&sender_keys.npk(), 0),
);
let recipient_pre =
AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk());
AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0));
let esk_1 = [3; 32];
let shared_secret_1 = SharedSecretKey::new(&esk_1, &sender_keys.vpk());
@ -1339,8 +1345,8 @@ pub mod tests {
Program::serialize_instruction(balance_to_move).unwrap(),
vec![1, 2],
vec![
(sender_keys.npk(), shared_secret_1),
(recipient_keys.npk(), shared_secret_2),
(sender_keys.npk(), 0, shared_secret_1),
(recipient_keys.npk(), 0, shared_secret_2),
],
vec![sender_keys.nsk],
vec![state.get_proof_for_commitment(&sender_commitment), None],
@ -1372,9 +1378,13 @@ pub mod tests {
state: &V03State,
) -> PrivacyPreservingTransaction {
let program = Program::authenticated_transfer_program();
let sender_commitment = Commitment::new(&sender_keys.npk(), sender_private_account);
let sender_pre =
AccountWithMetadata::new(sender_private_account.clone(), true, &sender_keys.npk());
let sender_account_id = AccountId::from((&sender_keys.npk(), 0));
let sender_commitment = Commitment::new(&sender_account_id, sender_private_account);
let sender_pre = AccountWithMetadata::new(
sender_private_account.clone(),
true,
(&sender_keys.npk(), 0),
);
let recipient_pre = AccountWithMetadata::new(
state.get_account_by_id(*recipient_account_id),
false,
@ -1389,7 +1399,7 @@ pub mod tests {
vec![sender_pre, recipient_pre],
Program::serialize_instruction(balance_to_move).unwrap(),
vec![1, 0],
vec![(sender_keys.npk(), shared_secret)],
vec![(sender_keys.npk(), 0, shared_secret)],
vec![sender_keys.nsk],
vec![state.get_proof_for_commitment(&sender_commitment)],
&program.into(),
@ -1476,8 +1486,10 @@ pub mod tests {
&state,
);
let sender_account_id = AccountId::from((&sender_keys.npk(), 0));
let recipient_account_id = AccountId::from((&recipient_keys.npk(), 0));
let expected_new_commitment_1 = Commitment::new(
&sender_keys.npk(),
&sender_account_id,
&Account {
program_owner: Program::authenticated_transfer_program().id(),
nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk),
@ -1486,15 +1498,15 @@ pub mod tests {
},
);
let sender_pre_commitment = Commitment::new(&sender_keys.npk(), &sender_private_account);
let sender_pre_commitment = Commitment::new(&sender_account_id, &sender_private_account);
let expected_new_nullifier =
Nullifier::for_account_update(&sender_pre_commitment, &sender_keys.nsk);
let expected_new_commitment_2 = Commitment::new(
&recipient_keys.npk(),
&recipient_account_id,
&Account {
program_owner: Program::authenticated_transfer_program().id(),
nonce: Nonce::private_account_nonce_init(&recipient_keys.npk()),
nonce: Nonce::private_account_nonce_init(&recipient_account_id),
balance: balance_to_move,
..Account::default()
},
@ -1553,8 +1565,9 @@ pub mod tests {
&state,
);
let sender_account_id = AccountId::from((&sender_keys.npk(), 0));
let expected_new_commitment = Commitment::new(
&sender_keys.npk(),
&sender_account_id,
&Account {
program_owner: Program::authenticated_transfer_program().id(),
nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk),
@ -1563,7 +1576,7 @@ pub mod tests {
},
);
let sender_pre_commitment = Commitment::new(&sender_keys.npk(), &sender_private_account);
let sender_pre_commitment = Commitment::new(&sender_account_id, &sender_private_account);
let expected_new_nullifier =
Nullifier::for_account_update(&sender_pre_commitment, &sender_keys.nsk);
@ -1895,10 +1908,10 @@ pub mod tests {
..Account::default()
},
true,
&sender_keys.npk(),
(&sender_keys.npk(), 0),
);
let private_account_2 =
AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk());
AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0));
let result = execute_and_prove(
vec![private_account_1, private_account_2],
@ -1907,10 +1920,12 @@ pub mod tests {
vec![
(
sender_keys.npk(),
0,
SharedSecretKey::new(&[55; 32], &sender_keys.vpk()),
),
(
recipient_keys.npk(),
0,
SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()),
),
],
@ -1933,7 +1948,7 @@ pub mod tests {
..Account::default()
},
true,
&sender_keys.npk(),
(&sender_keys.npk(), 0),
);
let private_account_2 =
AccountWithMetadata::new(Account::default(), false, AccountId::new([1; 32]));
@ -1941,6 +1956,7 @@ pub mod tests {
// Setting only one key for an execution with two private accounts.
let private_account_keys = [(
sender_keys.npk(),
0,
SharedSecretKey::new(&[55; 32], &sender_keys.vpk()),
)];
let result = execute_and_prove(
@ -1968,10 +1984,10 @@ pub mod tests {
..Account::default()
},
true,
&sender_keys.npk(),
(&sender_keys.npk(), 0),
);
let private_account_2 =
AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk());
AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0));
// Setting no second commitment proof.
let private_account_membership_proofs = [Some((0, vec![]))];
@ -1982,10 +1998,12 @@ pub mod tests {
vec![
(
sender_keys.npk(),
0,
SharedSecretKey::new(&[55; 32], &sender_keys.vpk()),
),
(
recipient_keys.npk(),
0,
SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()),
),
],
@ -2009,10 +2027,10 @@ pub mod tests {
..Account::default()
},
true,
&sender_keys.npk(),
(&sender_keys.npk(), 0),
);
let private_account_2 =
AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk());
AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0));
// Setting no auth key for an execution with one non default private accounts.
let private_account_nsks = [];
@ -2023,10 +2041,12 @@ pub mod tests {
vec![
(
sender_keys.npk(),
0,
SharedSecretKey::new(&[55; 32], &sender_keys.vpk()),
),
(
recipient_keys.npk(),
0,
SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()),
),
],
@ -2050,20 +2070,22 @@ pub mod tests {
..Account::default()
},
true,
&sender_keys.npk(),
(&sender_keys.npk(), 0),
);
let private_account_2 =
AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk());
AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0));
let private_account_keys = [
// First private account is the sender
(
sender_keys.npk(),
0,
SharedSecretKey::new(&[55; 32], &sender_keys.vpk()),
),
// Second private account is the recipient
(
recipient_keys.npk(),
0,
SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()),
),
];
@ -2098,7 +2120,7 @@ pub mod tests {
..Account::default()
},
true,
&sender_keys.npk(),
(&sender_keys.npk(), 0),
);
let private_account_2 = AccountWithMetadata::new(
Account {
@ -2107,7 +2129,7 @@ pub mod tests {
..Account::default()
},
false,
&recipient_keys.npk(),
(&recipient_keys.npk(), 0),
);
let result = execute_and_prove(
@ -2117,10 +2139,12 @@ pub mod tests {
vec![
(
sender_keys.npk(),
0,
SharedSecretKey::new(&[55; 32], &sender_keys.vpk()),
),
(
recipient_keys.npk(),
0,
SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()),
),
],
@ -2144,7 +2168,7 @@ pub mod tests {
..Account::default()
},
true,
&sender_keys.npk(),
(&sender_keys.npk(), 0),
);
let private_account_2 = AccountWithMetadata::new(
Account {
@ -2153,7 +2177,7 @@ pub mod tests {
..Account::default()
},
false,
&recipient_keys.npk(),
(&recipient_keys.npk(), 0),
);
let result = execute_and_prove(
@ -2163,10 +2187,12 @@ pub mod tests {
vec![
(
sender_keys.npk(),
0,
SharedSecretKey::new(&[55; 32], &sender_keys.vpk()),
),
(
recipient_keys.npk(),
0,
SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()),
),
],
@ -2190,7 +2216,7 @@ pub mod tests {
..Account::default()
},
true,
&sender_keys.npk(),
(&sender_keys.npk(), 0),
);
let private_account_2 = AccountWithMetadata::new(
Account {
@ -2199,7 +2225,7 @@ pub mod tests {
..Account::default()
},
false,
&recipient_keys.npk(),
(&recipient_keys.npk(), 0),
);
let result = execute_and_prove(
@ -2209,10 +2235,12 @@ pub mod tests {
vec![
(
sender_keys.npk(),
0,
SharedSecretKey::new(&[55; 32], &sender_keys.vpk()),
),
(
recipient_keys.npk(),
0,
SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()),
),
],
@ -2236,7 +2264,7 @@ pub mod tests {
..Account::default()
},
true,
&sender_keys.npk(),
(&sender_keys.npk(), 0),
);
let private_account_2 = AccountWithMetadata::new(
Account {
@ -2245,7 +2273,7 @@ pub mod tests {
..Account::default()
},
false,
&recipient_keys.npk(),
(&recipient_keys.npk(), 0),
);
let result = execute_and_prove(
@ -2255,10 +2283,12 @@ pub mod tests {
vec![
(
sender_keys.npk(),
0,
SharedSecretKey::new(&[55; 32], &sender_keys.vpk()),
),
(
recipient_keys.npk(),
0,
SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()),
),
],
@ -2283,13 +2313,13 @@ pub mod tests {
..Account::default()
},
true,
&sender_keys.npk(),
(&sender_keys.npk(), 0),
);
let private_account_2 = AccountWithMetadata::new(
Account::default(),
// This should be set to false in normal circumstances
true,
&recipient_keys.npk(),
(&recipient_keys.npk(), 0),
);
let result = execute_and_prove(
@ -2299,10 +2329,12 @@ pub mod tests {
vec![
(
sender_keys.npk(),
0,
SharedSecretKey::new(&[55; 32], &sender_keys.vpk()),
),
(
recipient_keys.npk(),
0,
SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()),
),
],
@ -2341,7 +2373,7 @@ pub mod tests {
vec![public_account_1, private_pda_account],
Program::serialize_instruction(10_u128).unwrap(),
visibility_mask.to_vec(),
vec![(npk, shared_secret)],
vec![(npk, 0, shared_secret)],
vec![],
vec![None],
&program.into(),
@ -2370,7 +2402,7 @@ pub mod tests {
vec![pre_state],
Program::serialize_instruction(seed).unwrap(),
vec![3],
vec![(npk, shared_secret)],
vec![(npk, u128::MAX, shared_secret)],
vec![],
vec![None],
&program.into(),
@ -2408,7 +2440,7 @@ pub mod tests {
vec![pre_state],
Program::serialize_instruction(seed).unwrap(),
vec![3],
vec![(npk_b, shared_secret)],
vec![(npk_b, 0, shared_secret)],
vec![],
vec![None],
&program.into(),
@ -2442,7 +2474,7 @@ pub mod tests {
vec![pre_state],
Program::serialize_instruction((seed, seed, callee_id)).unwrap(),
vec![3],
vec![(npk, shared_secret)],
vec![(npk, u128::MAX, shared_secret)],
vec![],
vec![None],
&program_with_deps,
@ -2479,7 +2511,7 @@ pub mod tests {
vec![pre_state],
Program::serialize_instruction((claim_seed, wrong_delegated_seed, callee_id)).unwrap(),
vec![3],
vec![(npk, shared_secret)],
vec![(npk, 0, shared_secret)],
vec![],
vec![None],
&program_with_deps,
@ -2515,7 +2547,7 @@ pub mod tests {
vec![pre_a, pre_b],
Program::serialize_instruction(seed).unwrap(),
vec![3, 3],
vec![(keys_a.npk(), shared_a), (keys_b.npk(), shared_b)],
vec![(keys_a.npk(), 0, shared_a), (keys_b.npk(), 0, shared_b)],
vec![],
vec![None, None],
&program.into(),
@ -2559,7 +2591,7 @@ pub mod tests {
vec![owned_pre_state],
Program::serialize_instruction(()).unwrap(),
vec![3],
vec![(npk, shared_secret)],
vec![(npk, 0, shared_secret)],
vec![],
vec![None],
&program.into(),
@ -2579,10 +2611,10 @@ pub mod tests {
..Account::default()
},
true,
&sender_keys.npk(),
(&sender_keys.npk(), 0),
);
let private_account_2 =
AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk());
AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0));
let result = execute_and_prove(
vec![private_account_1, private_account_2],
@ -2591,10 +2623,12 @@ pub mod tests {
vec![
(
sender_keys.npk(),
0,
SharedSecretKey::new(&[55; 32], &sender_keys.vpk()),
),
(
recipient_keys.npk(),
0,
SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()),
),
],
@ -2618,24 +2652,27 @@ pub mod tests {
..Account::default()
},
true,
&sender_keys.npk(),
(&sender_keys.npk(), 0),
);
let private_account_2 =
AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk());
AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0));
// Setting three private account keys for a circuit execution with only two private
// accounts.
let private_account_keys = [
(
sender_keys.npk(),
0,
SharedSecretKey::new(&[55; 32], &sender_keys.vpk()),
),
(
recipient_keys.npk(),
0,
SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()),
),
(
sender_keys.npk(),
0,
SharedSecretKey::new(&[57; 32], &sender_keys.vpk()),
),
];
@ -2664,10 +2701,10 @@ pub mod tests {
..Account::default()
},
true,
&sender_keys.npk(),
(&sender_keys.npk(), 0),
);
let private_account_2 =
AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk());
AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0));
// Setting two private account keys for a circuit execution with only one non default
// private account (visibility mask equal to 1 means that auth keys are expected).
@ -2681,10 +2718,12 @@ pub mod tests {
vec![
(
sender_keys.npk(),
0,
SharedSecretKey::new(&[55; 32], &sender_keys.vpk()),
),
(
recipient_keys.npk(),
0,
SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()),
),
],
@ -2763,7 +2802,7 @@ pub mod tests {
..Account::default()
},
true,
&sender_keys.npk(),
(&sender_keys.npk(), 0),
);
let visibility_mask = [1, 1];
@ -2775,8 +2814,8 @@ pub mod tests {
Program::serialize_instruction(100_u128).unwrap(),
visibility_mask.to_vec(),
vec![
(sender_keys.npk(), shared_secret),
(sender_keys.npk(), shared_secret),
(sender_keys.npk(), 0, shared_secret),
(sender_keys.npk(), 0, shared_secret),
],
private_account_nsks.to_vec(),
private_account_membership_proofs.to_vec(),
@ -3090,14 +3129,16 @@ pub mod tests {
balance: 100,
..Account::default()
};
let sender_commitment = Commitment::new(&sender_keys.npk(), &sender_private_account);
let sender_init_nullifier = Nullifier::for_account_initialization(&sender_keys.npk());
let sender_account_id = AccountId::from((&sender_keys.npk(), 0));
let sender_commitment = Commitment::new(&sender_account_id, &sender_private_account);
let sender_init_nullifier = Nullifier::for_account_initialization(&sender_account_id);
let mut state = V03State::new_with_genesis_accounts(
&[],
vec![(sender_commitment.clone(), sender_init_nullifier)],
0,
);
let sender_pre = AccountWithMetadata::new(sender_private_account, true, &sender_keys.npk());
let sender_pre =
AccountWithMetadata::new(sender_private_account, true, (&sender_keys.npk(), 0));
let recipient_private_key = PrivateKey::try_new([2; 32]).unwrap();
let recipient_account_id =
AccountId::from(&PublicKey::new_from_private_key(&recipient_private_key));
@ -3111,7 +3152,7 @@ pub mod tests {
vec![sender_pre, recipient_pre],
Program::serialize_instruction(37_u128).unwrap(),
vec![1, 0],
vec![(sender_keys.npk(), shared_secret)],
vec![(sender_keys.npk(), 0, shared_secret)],
vec![sender_keys.nsk],
vec![state.get_proof_for_commitment(&sender_commitment)],
&program.into(),
@ -3163,7 +3204,7 @@ pub mod tests {
..Account::default()
},
true,
&from_keys.npk(),
(&from_keys.npk(), 0),
);
let to_account = AccountWithMetadata::new(
Account {
@ -3171,13 +3212,15 @@ pub mod tests {
..Account::default()
},
true,
&to_keys.npk(),
(&to_keys.npk(), 0),
);
let from_commitment = Commitment::new(&from_keys.npk(), &from_account.account);
let to_commitment = Commitment::new(&to_keys.npk(), &to_account.account);
let from_init_nullifier = Nullifier::for_account_initialization(&from_keys.npk());
let to_init_nullifier = Nullifier::for_account_initialization(&to_keys.npk());
let from_account_id = AccountId::from((&from_keys.npk(), 0));
let to_account_id = AccountId::from((&to_keys.npk(), 0));
let from_commitment = Commitment::new(&from_account_id, &from_account.account);
let to_commitment = Commitment::new(&to_account_id, &to_account.account);
let from_init_nullifier = Nullifier::for_account_initialization(&from_account_id);
let to_init_nullifier = Nullifier::for_account_initialization(&to_account_id);
let mut state = V03State::new_with_genesis_accounts(
&[],
vec![
@ -3216,21 +3259,21 @@ pub mod tests {
nonce: from_new_nonce,
..from_account.account.clone()
};
let from_expected_commitment = Commitment::new(&from_keys.npk(), &from_expected_post);
let from_expected_commitment = Commitment::new(&from_account_id, &from_expected_post);
let to_expected_post = Account {
balance: u128::from(number_of_calls) * amount,
nonce: to_new_nonce,
..to_account.account.clone()
};
let to_expected_commitment = Commitment::new(&to_keys.npk(), &to_expected_post);
let to_expected_commitment = Commitment::new(&to_account_id, &to_expected_post);
// Act
let (output, proof) = execute_and_prove(
vec![to_account, from_account],
Program::serialize_instruction(instruction).unwrap(),
vec![1, 1],
vec![(from_keys.npk(), to_ss), (to_keys.npk(), from_ss)],
vec![(from_keys.npk(), 0, to_ss), (to_keys.npk(), 0, from_ss)],
vec![from_keys.nsk, to_keys.nsk],
vec![
state.get_proof_for_commitment(&from_commitment),
@ -3482,7 +3525,7 @@ pub mod tests {
// Create an authorized private account with default values (new account being initialized)
let authorized_account =
AccountWithMetadata::new(Account::default(), true, &private_keys.npk());
AccountWithMetadata::new(Account::default(), true, (&private_keys.npk(), 0));
let program = Program::authenticated_transfer_program();
@ -3499,7 +3542,7 @@ pub mod tests {
vec![authorized_account],
Program::serialize_instruction(balance).unwrap(),
vec![1],
vec![(private_keys.npk(), shared_secret)],
vec![(private_keys.npk(), 0, shared_secret)],
vec![private_keys.nsk],
vec![None],
&program.into(),
@ -3521,7 +3564,8 @@ pub mod tests {
let result = state.transition_from_privacy_preserving_transaction(&tx, 1, 0);
assert!(result.is_ok());
let nullifier = Nullifier::for_account_initialization(&private_keys.npk());
let account_id = AccountId::from((&private_keys.npk(), 0));
let nullifier = Nullifier::for_account_initialization(&account_id);
assert!(state.private_state.1.contains(&nullifier));
}
@ -3535,7 +3579,7 @@ pub mod tests {
// operate them without the corresponding private keys, so unauthorized private claiming
// remains allowed.
let unauthorized_account =
AccountWithMetadata::new(Account::default(), false, &private_keys.npk());
AccountWithMetadata::new(Account::default(), false, (&private_keys.npk(), 0));
let program = Program::claimer();
let esk = [5; 32];
@ -3546,7 +3590,7 @@ pub mod tests {
vec![unauthorized_account],
Program::serialize_instruction(0_u128).unwrap(),
vec![2],
vec![(private_keys.npk(), shared_secret)],
vec![(private_keys.npk(), 0, shared_secret)],
vec![],
vec![None],
&program.into(),
@ -3568,7 +3612,8 @@ pub mod tests {
.transition_from_privacy_preserving_transaction(&tx, 1, 0)
.unwrap();
let nullifier = Nullifier::for_account_initialization(&private_keys.npk());
let account_id = AccountId::from((&private_keys.npk(), 0));
let nullifier = Nullifier::for_account_initialization(&account_id);
assert!(state.private_state.1.contains(&nullifier));
}
@ -3581,7 +3626,7 @@ pub mod tests {
// Step 1: Create a new private account with authorization
let authorized_account =
AccountWithMetadata::new(Account::default(), true, &private_keys.npk());
AccountWithMetadata::new(Account::default(), true, (&private_keys.npk(), 0));
let claimer_program = Program::claimer();
@ -3597,7 +3642,7 @@ pub mod tests {
vec![authorized_account.clone()],
Program::serialize_instruction(balance).unwrap(),
vec![1],
vec![(private_keys.npk(), shared_secret)],
vec![(private_keys.npk(), 0, shared_secret)],
vec![private_keys.nsk],
vec![None],
&claimer_program.into(),
@ -3623,7 +3668,8 @@ pub mod tests {
);
// Verify the account is now initialized (nullifier exists)
let nullifier = Nullifier::for_account_initialization(&private_keys.npk());
let account_id = AccountId::from((&private_keys.npk(), 0));
let nullifier = Nullifier::for_account_initialization(&account_id);
assert!(state.private_state.1.contains(&nullifier));
// Prepare new state of account
@ -3642,7 +3688,7 @@ pub mod tests {
vec![account_metadata],
Program::serialize_instruction(()).unwrap(),
vec![1],
vec![(private_keys.npk(), shared_secret2)],
vec![(private_keys.npk(), 0, shared_secret2)],
vec![private_keys.nsk],
vec![None],
&noop_program.into(),
@ -3710,7 +3756,7 @@ pub mod tests {
let program = Program::changer_claimer();
let sender_keys = test_private_account_keys_1();
let private_account =
AccountWithMetadata::new(Account::default(), true, &sender_keys.npk());
AccountWithMetadata::new(Account::default(), true, (&sender_keys.npk(), 0));
// Don't change data (None) and don't claim (false)
let instruction: (Option<Vec<u8>>, bool) = (None, false);
@ -3720,6 +3766,7 @@ pub mod tests {
vec![1],
vec![(
sender_keys.npk(),
0,
SharedSecretKey::new(&[3; 32], &sender_keys.vpk()),
)],
vec![sender_keys.nsk],
@ -3736,7 +3783,7 @@ pub mod tests {
let program = Program::changer_claimer();
let sender_keys = test_private_account_keys_1();
let private_account =
AccountWithMetadata::new(Account::default(), true, &sender_keys.npk());
AccountWithMetadata::new(Account::default(), true, (&sender_keys.npk(), 0));
// Change data but don't claim (false) - should fail
let new_data = vec![1, 2, 3, 4, 5];
let instruction: (Option<Vec<u8>>, bool) = (Some(new_data), false);
@ -3747,6 +3794,7 @@ pub mod tests {
vec![1],
vec![(
sender_keys.npk(),
0,
SharedSecretKey::new(&[3; 32], &sender_keys.vpk()),
)],
vec![sender_keys.nsk],
@ -3776,11 +3824,12 @@ pub mod tests {
sender_keys.account_id(),
);
let recipient_account =
AccountWithMetadata::new(Account::default(), true, &recipient_keys.npk());
AccountWithMetadata::new(Account::default(), true, (&recipient_keys.npk(), 0));
let recipient_account_id = AccountId::from((&recipient_keys.npk(), 0));
let recipient_commitment =
Commitment::new(&recipient_keys.npk(), &recipient_account.account);
let recipient_init_nullifier = Nullifier::for_account_initialization(&recipient_keys.npk());
Commitment::new(&recipient_account_id, &recipient_account.account);
let recipient_init_nullifier = Nullifier::for_account_initialization(&recipient_account_id);
let state = V03State::new_with_genesis_accounts(
&[(sender_account.account_id, sender_account.account.balance)],
vec![(recipient_commitment.clone(), recipient_init_nullifier)],
@ -3803,7 +3852,7 @@ pub mod tests {
vec![sender_account, recipient_account],
Program::serialize_instruction(instruction).unwrap(),
vec![0, 1],
vec![(recipient_keys.npk(), recipient)],
vec![(recipient_keys.npk(), 0, recipient)],
vec![recipient_keys.nsk],
vec![state.get_proof_for_commitment(&recipient_commitment)],
&program_with_deps,
@ -3938,7 +3987,7 @@ pub mod tests {
let block_validity_window: BlockValidityWindow = validity_window.try_into().unwrap();
let validity_window_program = Program::validity_window();
let account_keys = test_private_account_keys_1();
let pre = AccountWithMetadata::new(Account::default(), false, &account_keys.npk());
let pre = AccountWithMetadata::new(Account::default(), false, (&account_keys.npk(), 0));
let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs();
let tx = {
let esk = [3; 32];
@ -3953,7 +4002,7 @@ pub mod tests {
vec![pre],
Program::serialize_instruction(instruction).unwrap(),
vec![2],
vec![(account_keys.npk(), shared_secret)],
vec![(account_keys.npk(), 0, shared_secret)],
vec![],
vec![None],
&validity_window_program.into(),
@ -4007,7 +4056,7 @@ pub mod tests {
validity_window.try_into().unwrap();
let validity_window_program = Program::validity_window();
let account_keys = test_private_account_keys_1();
let pre = AccountWithMetadata::new(Account::default(), false, &account_keys.npk());
let pre = AccountWithMetadata::new(Account::default(), false, (&account_keys.npk(), 0));
let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs();
let tx = {
let esk = [3; 32];
@ -4022,7 +4071,7 @@ pub mod tests {
vec![pre],
Program::serialize_instruction(instruction).unwrap(),
vec![2],
vec![(account_keys.npk(), shared_secret)],
vec![(account_keys.npk(), 0, shared_secret)],
vec![],
vec![None],
&validity_window_program.into(),

View File

@ -4,9 +4,9 @@ use std::{
};
use nssa_core::{
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, MembershipProof,
Nullifier, NullifierPublicKey, NullifierSecretKey, PrivacyPreservingCircuitInput,
PrivacyPreservingCircuitOutput, SharedSecretKey,
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, Identifier,
MembershipProof, Nullifier, NullifierPublicKey, NullifierSecretKey,
PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, SharedSecretKey,
account::{Account, AccountId, AccountWithMetadata, Nonce},
compute_digest_for_path,
program::{
@ -17,6 +17,8 @@ use nssa_core::{
};
use risc0_zkvm::{guest::env, serde::to_vec};
const PRIVATE_PDA_FIXED_IDENTIFIER: u128 = u128::MAX;
/// State of the involved accounts before and after program execution.
struct ExecutionState {
pre_states: Vec<AccountWithMetadata>,
@ -54,7 +56,7 @@ impl ExecutionState {
/// Validate program outputs and derive the overall execution state.
pub fn derive_from_outputs(
visibility_mask: &[u8],
private_account_keys: &[(NullifierPublicKey, SharedSecretKey)],
private_account_keys: &[(NullifierPublicKey, Identifier, SharedSecretKey)],
program_id: ProgramId,
program_outputs: Vec<ProgramOutput>,
) -> Self {
@ -67,7 +69,7 @@ impl ExecutionState {
let mut keys_iter = private_account_keys.iter();
for (pos, &mask) in visibility_mask.iter().enumerate() {
if matches!(mask, 1..=3) {
let (npk, _) = keys_iter.next().unwrap_or_else(|| {
let (npk, _, _) = keys_iter.next().unwrap_or_else(|| {
panic!(
"private_account_keys shorter than visibility_mask demands: no key for masked position {pos} (mask {mask})"
)
@ -487,7 +489,7 @@ fn resolve_authorization_and_record_bindings(
fn compute_circuit_output(
execution_state: ExecutionState,
visibility_mask: &[u8],
private_account_keys: &[(NullifierPublicKey, SharedSecretKey)],
private_account_keys: &[(NullifierPublicKey, Identifier, SharedSecretKey)],
private_account_nsks: &[NullifierSecretKey],
private_account_membership_proofs: &[Option<MembershipProof>],
) -> PrivacyPreservingCircuitOutput {
@ -523,16 +525,18 @@ fn compute_circuit_output(
output.public_post_states.push(post_state);
}
1 | 2 => {
let Some((npk, shared_secret)) = private_keys_iter.next() else {
let Some((npk, identifier, shared_secret)) = private_keys_iter.next() else {
panic!("Missing private account key");
};
assert_eq!(
AccountId::from(npk),
pre_state.account_id,
"AccountId mismatch"
assert_ne!(
*identifier, PRIVATE_PDA_FIXED_IDENTIFIER,
"Identifier must be different from {PRIVATE_PDA_FIXED_IDENTIFIER}. This is reserved for private PDA."
);
let account_id = AccountId::from((npk, *identifier));
assert_eq!(account_id, pre_state.account_id, "AccountId mismatch");
let (new_nullifier, new_nonce) = if account_visibility_mask == 1 {
// Private account with authentication
@ -560,7 +564,7 @@ fn compute_circuit_output(
let new_nullifier = compute_nullifier_and_set_digest(
membership_proof_opt.as_ref(),
&pre_state.account,
npk,
&account_id,
nsk,
);
@ -590,9 +594,9 @@ fn compute_circuit_output(
"Membership proof must be None for unauthorized accounts"
);
let nullifier = Nullifier::for_account_initialization(npk);
let nullifier = Nullifier::for_account_initialization(&account_id);
let new_nonce = Nonce::private_account_nonce_init(npk);
let new_nonce = Nonce::private_account_nonce_init(&account_id);
((nullifier, DUMMY_COMMITMENT_HASH), new_nonce)
};
@ -603,11 +607,12 @@ fn compute_circuit_output(
post_with_updated_nonce.nonce = new_nonce;
// Compute commitment
let commitment_post = Commitment::new(npk, &post_with_updated_nonce);
let commitment_post = Commitment::new(&account_id, &post_with_updated_nonce);
// Encrypt and push post state
let encrypted_account = EncryptionScheme::encrypt(
&post_with_updated_nonce,
*identifier,
shared_secret,
&commitment_post,
output_index,
@ -628,10 +633,15 @@ fn compute_circuit_output(
// `private_pda_bound_positions` check) guarantees that every mask-3
// position has been through at least one such binding, so this
// branch can safely use the wallet npk without re-verifying.
let Some((npk, shared_secret)) = private_keys_iter.next() else {
let Some((npk, identifier, shared_secret)) = private_keys_iter.next() else {
panic!("Missing private account key");
};
assert_eq!(
*identifier, PRIVATE_PDA_FIXED_IDENTIFIER,
"Identifier for private PDAs must be {PRIVATE_PDA_FIXED_IDENTIFIER}."
);
let (new_nullifier, new_nonce) = if pre_state.is_authorized {
// Existing private PDA with authentication (like mask 1)
let Some(nsk) = private_nsks_iter.next() else {
@ -650,7 +660,7 @@ fn compute_circuit_output(
let new_nullifier = compute_nullifier_and_set_digest(
membership_proof_opt.as_ref(),
&pre_state.account,
npk,
&pre_state.account_id,
nsk,
);
let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk);
@ -677,8 +687,8 @@ fn compute_circuit_output(
"Membership proof must be None for new accounts"
);
let nullifier = Nullifier::for_account_initialization(npk);
let new_nonce = Nonce::private_account_nonce_init(npk);
let nullifier = Nullifier::for_account_initialization(&pre_state.account_id);
let new_nonce = Nonce::private_account_nonce_init(&pre_state.account_id);
((nullifier, DUMMY_COMMITMENT_HASH), new_nonce)
};
output.new_nullifiers.push(new_nullifier);
@ -686,10 +696,12 @@ fn compute_circuit_output(
let mut post_with_updated_nonce = post_state;
post_with_updated_nonce.nonce = new_nonce;
let commitment_post = Commitment::new(npk, &post_with_updated_nonce);
let commitment_post =
Commitment::new(&pre_state.account_id, &post_with_updated_nonce);
let encrypted_account = EncryptionScheme::encrypt(
&post_with_updated_nonce,
PRIVATE_PDA_FIXED_IDENTIFIER,
shared_secret,
&commitment_post,
output_index,
@ -726,7 +738,7 @@ fn compute_circuit_output(
fn compute_nullifier_and_set_digest(
membership_proof_opt: Option<&MembershipProof>,
pre_account: &Account,
npk: &NullifierPublicKey,
account_id: &AccountId,
nsk: &NullifierSecretKey,
) -> (Nullifier, CommitmentSetDigest) {
membership_proof_opt.as_ref().map_or_else(
@ -738,12 +750,12 @@ fn compute_nullifier_and_set_digest(
);
// Compute initialization nullifier
let nullifier = Nullifier::for_account_initialization(npk);
let nullifier = Nullifier::for_account_initialization(account_id);
(nullifier, DUMMY_COMMITMENT_HASH)
},
|membership_proof| {
// Compute commitment set digest associated with provided auth path
let commitment_pre = Commitment::new(npk, pre_account);
let commitment_pre = Commitment::new(account_id, pre_account);
let set_digest = compute_digest_for_path(&commitment_pre, membership_proof);
// Compute update nullifier

View File

@ -110,6 +110,7 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
.iter()
.map(|init_comm_data| {
let npk = &init_comm_data.npk;
let account_id = nssa::AccountId::from((npk, 0));
let mut acc = init_comm_data.account.clone();
@ -117,8 +118,8 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
nssa::program::Program::authenticated_transfer_program().id();
(
nssa_core::Commitment::new(npk, &acc),
nssa_core::Nullifier::for_account_initialization(npk),
nssa_core::Commitment::new(&account_id, &acc),
nssa_core::Nullifier::for_account_initialization(&account_id),
)
})
.collect()
@ -1107,10 +1108,14 @@ mod tests {
let epk = EphemeralPublicKey::from_scalar(esk);
let (output, proof) = execute_and_prove(
vec![AccountWithMetadata::new(Account::default(), true, &npk)],
vec![AccountWithMetadata::new(
Account::default(),
true,
(&npk, 0),
)],
Program::serialize_instruction(0_u128).unwrap(),
vec![1],
vec![(npk, shared_secret)],
vec![(npk, 0, shared_secret)],
vec![nsk],
vec![None],
&Program::authenticated_transfer_program().into(),

View File

@ -95,9 +95,16 @@ pub struct PublicAccountPrivateInitialData {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrivateAccountPrivateInitialData {
pub account_id: nssa::AccountId,
pub account: nssa_core::account::Account,
pub key_chain: KeyChain,
pub identifier: nssa_core::Identifier,
}
impl PrivateAccountPrivateInitialData {
#[must_use]
pub fn account_id(&self) -> nssa::AccountId {
nssa::AccountId::from((&self.key_chain.nullifier_public_key, self.identifier))
}
}
#[must_use]
@ -142,7 +149,6 @@ pub fn initial_priv_accounts_private_keys() -> Vec<PrivateAccountPrivateInitialD
vec![
PrivateAccountPrivateInitialData {
account_id: AccountId::from(&key_chain_1.nullifier_public_key),
account: Account {
program_owner: DEFAULT_PROGRAM_OWNER,
balance: PRIV_ACC_A_INITIAL_BALANCE,
@ -150,9 +156,9 @@ pub fn initial_priv_accounts_private_keys() -> Vec<PrivateAccountPrivateInitialD
nonce: 0.into(),
},
key_chain: key_chain_1,
identifier: 0,
},
PrivateAccountPrivateInitialData {
account_id: AccountId::from(&key_chain_2.nullifier_public_key),
account: Account {
program_owner: DEFAULT_PROGRAM_OWNER,
balance: PRIV_ACC_B_INITIAL_BALANCE,
@ -160,6 +166,7 @@ pub fn initial_priv_accounts_private_keys() -> Vec<PrivateAccountPrivateInitialD
nonce: 0.into(),
},
key_chain: key_chain_2,
identifier: 0,
},
]
}
@ -201,14 +208,15 @@ pub fn initial_state() -> V03State {
.iter()
.map(|init_comm_data| {
let npk = &init_comm_data.npk;
let account_id = nssa::AccountId::from((npk, 0));
let mut acc = init_comm_data.account.clone();
acc.program_owner = nssa::program::Program::authenticated_transfer_program().id();
(
nssa_core::Commitment::new(npk, &acc),
nssa_core::Nullifier::for_account_initialization(npk),
nssa_core::Commitment::new(&account_id, &acc),
nssa_core::Nullifier::for_account_initialization(&account_id),
)
})
.collect();
@ -239,8 +247,8 @@ mod tests {
const PUB_ACC_A_TEXT_ADDR: &str = "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV";
const PUB_ACC_B_TEXT_ADDR: &str = "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo";
const PRIV_ACC_A_TEXT_ADDR: &str = "5ya25h4Xc9GAmrGB2WrTEnEWtQKJwRwQx3Xfo2tucNcE";
const PRIV_ACC_B_TEXT_ADDR: &str = "E8HwiTyQe4H9HK7icTvn95HQMnzx49mP9A2ddtMLpNaN";
const PRIV_ACC_A_TEXT_ADDR: &str = "4eGX3M3rgjHsme8n3sSp89af8JRZtYVTesbJjLqaX1VQ";
const PRIV_ACC_B_TEXT_ADDR: &str = "3m6HQmCgmAvsxZtxAHPqqEqoBG4335fCG8TzxigyW7rE";
#[test]
fn pub_state_consistency() {
@ -354,11 +362,11 @@ mod tests {
);
assert_eq!(
init_private_accs_keys[0].account_id.to_string(),
init_private_accs_keys[0].account_id().to_string(),
PRIV_ACC_A_TEXT_ADDR
);
assert_eq!(
init_private_accs_keys[1].account_id.to_string(),
init_private_accs_keys[1].account_id().to_string(),
PRIV_ACC_B_TEXT_ADDR
);

View File

@ -7,7 +7,10 @@ use nssa::AccountId;
use crate::{
block_on,
error::{print_error, WalletFfiError},
types::{FfiAccount, FfiAccountList, FfiAccountListEntry, FfiBytes32, WalletHandle},
types::{
FfiAccount, FfiAccountList, FfiAccountListEntry, FfiBytes32, FfiPrivateAccountKeys,
WalletHandle,
},
wallet::get_wallet,
};
@ -59,10 +62,18 @@ pub unsafe extern "C" fn wallet_ffi_create_account_public(
WalletFfiError::Success
}
/// Create a new private account.
/// Create a new private account, storing a default account entry in local storage.
///
/// Private accounts use privacy-preserving transactions with nullifiers
/// and commitments.
/// This is the private-account equivalent of `wallet_ffi_create_account_public`.
/// It generates a key node, assigns a random identifier, and inserts a default
/// account record so the account can immediately be used with
/// `wallet_ffi_register_private_account`.
///
/// The identifier is chosen at random and is not encoded in the mnemonic seed.
/// Once the account is initialized, the identifier is embedded in the encrypted
/// transaction payload and can be recovered by running `sync-private` from the
/// same mnemonic. An account that was created locally but has never been initialized
/// cannot be recovered from the seed alone.
///
/// # Parameters
/// - `handle`: Valid wallet handle
@ -107,6 +118,78 @@ pub unsafe extern "C" fn wallet_ffi_create_account_private(
WalletFfiError::Success
}
/// Create a new private key node.
///
/// Returns the nullifier public key (npk) and viewing public key (vpk) to share with
/// senders. Account IDs are discovered later via sync when senders initialize accounts
/// under this key.
///
/// # Parameters
/// - `handle`: Valid wallet handle
/// - `out_keys`: Output pointer for the key data (npk + vpk)
///
/// # Returns
/// - `Success` on successful creation
/// - Error code on failure
///
/// # Memory
/// The keys structure must be freed with `wallet_ffi_free_private_account_keys()`.
///
/// # Safety
/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open`
/// - `out_keys` must be a valid pointer to a `FfiPrivateAccountKeys` struct
#[no_mangle]
pub unsafe extern "C" fn wallet_ffi_create_private_accounts_key(
handle: *mut WalletHandle,
out_keys: *mut FfiPrivateAccountKeys,
) -> WalletFfiError {
let wrapper = match get_wallet(handle) {
Ok(w) => w,
Err(e) => return e,
};
if out_keys.is_null() {
print_error("Null output pointer for keys");
return WalletFfiError::NullPointer;
}
let mut wallet = match wrapper.core.lock() {
Ok(w) => w,
Err(e) => {
print_error(format!("Failed to lock wallet: {e}"));
return WalletFfiError::InternalError;
}
};
let chain_index = wallet.create_private_accounts_key(None);
let node = wallet
.storage()
.user_data
.private_key_tree
.key_map
.get(&chain_index)
.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 {
(*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
}
/// List all accounts in the wallet.
///
/// Returns both public and private accounts managed by this wallet.

View File

@ -116,7 +116,8 @@ pub unsafe extern "C" fn wallet_ffi_get_private_account_keys(
let account_id = AccountId::new(unsafe { (*account_id).data });
let Some((key_chain, _account)) = wallet.storage().user_data.get_private_account(account_id)
let Some((key_chain, _account, _identifier)) =
wallet.storage().user_data.get_private_account(account_id)
else {
print_error("Private account not found in wallet");
return WalletFfiError::AccountNotFound;

View File

@ -9,7 +9,7 @@ use crate::{
block_on,
error::{print_error, WalletFfiError},
map_execution_error,
types::{FfiBytes32, FfiTransferResult, WalletHandle},
types::{FfiBytes32, FfiTransferResult, FfiU128, WalletHandle},
wallet::get_wallet,
FfiPrivateAccountKeys,
};
@ -102,6 +102,7 @@ pub unsafe extern "C" fn wallet_ffi_transfer_public(
/// - `handle`: Valid wallet handle
/// - `from`: Source account ID (must be owned by this wallet)
/// - `to_keys`: Destination account keys
/// - `to_identifier`: Identifier for the recipient's private account
/// - `amount`: Amount to transfer as little-endian [u8; 16]
/// - `out_result`: Output pointer for transfer result
///
@ -125,6 +126,7 @@ pub unsafe extern "C" fn wallet_ffi_transfer_shielded(
handle: *mut WalletHandle,
from: *const FfiBytes32,
to_keys: *const FfiPrivateAccountKeys,
to_identifier: *const FfiU128,
amount: *const [u8; 16],
out_result: *mut FfiTransferResult,
) -> WalletFfiError {
@ -133,7 +135,12 @@ pub unsafe extern "C" fn wallet_ffi_transfer_shielded(
Err(e) => return e,
};
if from.is_null() || to_keys.is_null() || amount.is_null() || out_result.is_null() {
if from.is_null()
|| to_keys.is_null()
|| to_identifier.is_null()
|| amount.is_null()
|| out_result.is_null()
{
print_error("Null pointer argument");
return WalletFfiError::NullPointer;
}
@ -155,13 +162,18 @@ pub unsafe extern "C" fn wallet_ffi_transfer_shielded(
return e;
}
};
let to_identifier = u128::from_le_bytes(unsafe { (*to_identifier).data });
let amount = u128::from_le_bytes(unsafe { *amount });
let transfer = NativeTokenTransfer(&wallet);
match block_on(
transfer.send_shielded_transfer_to_outer_account(from_id, to_npk, to_vpk, amount),
) {
match block_on(transfer.send_shielded_transfer_to_outer_account(
from_id,
to_npk,
to_vpk,
to_identifier,
amount,
)) {
Ok((tx_hash, _shared_key)) => {
let tx_hash = CString::new(tx_hash.to_string())
.map_or(ptr::null_mut(), std::ffi::CString::into_raw);
@ -271,6 +283,7 @@ pub unsafe extern "C" fn wallet_ffi_transfer_deshielded(
/// - `handle`: Valid wallet handle
/// - `from`: Source account ID (must be owned by this wallet)
/// - `to_keys`: Destination account keys
/// - `to_identifier`: Identifier for the recipient's private account
/// - `amount`: Amount to transfer as little-endian [u8; 16]
/// - `out_result`: Output pointer for transfer result
///
@ -294,6 +307,7 @@ pub unsafe extern "C" fn wallet_ffi_transfer_private(
handle: *mut WalletHandle,
from: *const FfiBytes32,
to_keys: *const FfiPrivateAccountKeys,
to_identifier: *const FfiU128,
amount: *const [u8; 16],
out_result: *mut FfiTransferResult,
) -> WalletFfiError {
@ -302,7 +316,12 @@ pub unsafe extern "C" fn wallet_ffi_transfer_private(
Err(e) => return e,
};
if from.is_null() || to_keys.is_null() || amount.is_null() || out_result.is_null() {
if from.is_null()
|| to_keys.is_null()
|| to_identifier.is_null()
|| amount.is_null()
|| out_result.is_null()
{
print_error("Null pointer argument");
return WalletFfiError::NullPointer;
}
@ -324,12 +343,18 @@ pub unsafe extern "C" fn wallet_ffi_transfer_private(
return e;
}
};
let to_identifier = u128::from_le_bytes(unsafe { (*to_identifier).data });
let amount = u128::from_le_bytes(unsafe { *amount });
let transfer = NativeTokenTransfer(&wallet);
match block_on(transfer.send_private_transfer_to_outer_account(from_id, to_npk, to_vpk, amount))
{
match block_on(transfer.send_private_transfer_to_outer_account(
from_id,
to_npk,
to_vpk,
to_identifier,
amount,
)) {
Ok((tx_hash, _shared_key)) => {
let tx_hash = CString::new(tx_hash.to_string())
.map_or(ptr::null_mut(), std::ffi::CString::into_raw);

View File

@ -126,6 +126,24 @@ typedef struct FfiBytes32 {
uint8_t data[32];
} 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.
*/
@ -189,24 +207,6 @@ typedef struct FfiPublicAccountKey {
struct FfiBytes32 public_key;
} 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.
*/
@ -243,10 +243,18 @@ enum WalletFfiError wallet_ffi_create_account_public(struct WalletHandle *handle
struct FfiBytes32 *out_account_id);
/**
* Create a new private account.
* Create a new private account, storing a default account entry in local storage.
*
* Private accounts use privacy-preserving transactions with nullifiers
* and commitments.
* This is the private-account equivalent of `wallet_ffi_create_account_public`.
* It generates a key node, assigns a random identifier, and inserts a default
* account record so the account can immediately be used with
* `wallet_ffi_register_private_account`.
*
* The identifier is chosen at random and is not encoded in the mnemonic seed.
* Once the account is initialized, the identifier is embedded in the encrypted
* transaction payload and can be recovered by running `sync-private` from the
* same mnemonic. An account that was created locally but has never been initialized
* cannot be recovered from the seed alone.
*
* # Parameters
* - `handle`: Valid wallet handle
@ -263,6 +271,31 @@ enum WalletFfiError wallet_ffi_create_account_public(struct WalletHandle *handle
enum WalletFfiError wallet_ffi_create_account_private(struct WalletHandle *handle,
struct FfiBytes32 *out_account_id);
/**
* Create a new private key node.
*
* Returns the nullifier public key (npk) and viewing public key (vpk) to share with
* senders. Account IDs are discovered later via sync when senders initialize accounts
* under this key.
*
* # Parameters
* - `handle`: Valid wallet handle
* - `out_keys`: Output pointer for the key data (npk + vpk)
*
* # Returns
* - `Success` on successful creation
* - Error code on failure
*
* # Memory
* The keys structure must be freed with `wallet_ffi_free_private_account_keys()`.
*
* # Safety
* - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open`
* - `out_keys` must be a valid pointer to a `FfiPrivateAccountKeys` struct
*/
enum WalletFfiError wallet_ffi_create_private_accounts_key(struct WalletHandle *handle,
struct FfiPrivateAccountKeys *out_keys);
/**
* List all accounts in the wallet.
*
@ -685,6 +718,7 @@ enum WalletFfiError wallet_ffi_transfer_public(struct WalletHandle *handle,
* - `handle`: Valid wallet handle
* - `from`: Source account ID (must be owned by this wallet)
* - `to_keys`: Destination account keys
* - `to_identifier`: Identifier for the recipient's private account
* - `amount`: Amount to transfer as little-endian [u8; 16]
* - `out_result`: Output pointer for transfer result
*
@ -707,6 +741,7 @@ enum WalletFfiError wallet_ffi_transfer_public(struct WalletHandle *handle,
enum WalletFfiError wallet_ffi_transfer_shielded(struct WalletHandle *handle,
const struct FfiBytes32 *from,
const struct FfiPrivateAccountKeys *to_keys,
const struct FfiU128 *to_identifier,
const uint8_t (*amount)[16],
struct FfiTransferResult *out_result);
@ -753,6 +788,7 @@ enum WalletFfiError wallet_ffi_transfer_deshielded(struct WalletHandle *handle,
* - `handle`: Valid wallet handle
* - `from`: Source account ID (must be owned by this wallet)
* - `to_keys`: Destination account keys
* - `to_identifier`: Identifier for the recipient's private account
* - `amount`: Amount to transfer as little-endian [u8; 16]
* - `out_result`: Output pointer for transfer result
*
@ -775,6 +811,7 @@ enum WalletFfiError wallet_ffi_transfer_deshielded(struct WalletHandle *handle,
enum WalletFfiError wallet_ffi_transfer_private(struct WalletHandle *handle,
const struct FfiBytes32 *from,
const struct FfiPrivateAccountKeys *to_keys,
const struct FfiU128 *to_identifier,
const uint8_t (*amount)[16],
struct FfiTransferResult *out_result);

View File

@ -19,7 +19,8 @@
},
{
"Private": {
"account_id": "9DGDXnrNo4QhUUb2F8WDuDrPESja3eYDkZG5HkzvAvMC",
"account_id": "GoKB6RuE6pT2KxCqDXQqiCuuuYZaGdJNfctzyqRdGBCy",
"identifier": 0,
"account": {
"program_owner": [
0,
@ -214,7 +215,8 @@
},
{
"Private": {
"account_id": "A6AT9UvsgitUi8w4BH43n6DyX1bK37DtSCfjEWXQQUrQ",
"account_id": "BCdMnPkdH2DrVhe7cGdawkPU9iapsSboRvJpWX8pWnLq",
"identifier": 0,
"account": {
"program_owner": [
0,

View File

@ -7,7 +7,7 @@ use key_protocol::{
key_tree::{KeyTreePrivate, KeyTreePublic, chain_index::ChainIndex},
secret_holders::SeedHolder,
},
key_protocol_core::NSSAUserData,
key_protocol_core::{NSSAUserData, UserPrivateAccountData},
};
use log::debug;
use nssa::program::Program;
@ -70,15 +70,28 @@ impl WalletChainStore {
public_tree.insert(data.account_id, data.chain_index, data.data);
}
PersistentAccountData::Private(data) => {
private_tree.insert(data.account_id, data.chain_index, data.data);
let npk = data.data.value.0.nullifier_public_key;
let chain_index = data.chain_index;
for identifier in &data.identifiers {
let account_id = nssa::AccountId::from((&npk, *identifier));
private_tree
.account_id_map
.insert(account_id, chain_index.clone());
}
private_tree.key_map.insert(chain_index, data.data);
}
PersistentAccountData::Preconfigured(acc_data) => match acc_data {
InitialAccountData::Public(data) => {
public_init_acc_map.insert(data.account_id, data.pub_sign_key);
}
InitialAccountData::Private(data) => {
private_init_acc_map
.insert(data.account_id, (data.key_chain, data.account));
private_init_acc_map.insert(
data.account_id(),
UserPrivateAccountData {
key_chain: data.key_chain,
accounts: vec![(data.identifier, data.account)],
},
);
}
},
}
@ -111,13 +124,20 @@ impl WalletChainStore {
public_init_acc_map.insert(data.account_id, data.pub_sign_key);
}
InitialAccountData::Private(data) => {
let account_id = data.account_id();
let mut account = data.account;
// TODO: Program owner is only known after code is compiled and can't be set
// in the config. Therefore we overwrite it here on
// startup. Fix this when program id can be fetched
// from the node and queried from the wallet.
account.program_owner = Program::authenticated_transfer_program().id();
private_init_acc_map.insert(data.account_id, (data.key_chain, account));
private_init_acc_map.insert(
account_id,
UserPrivateAccountData {
key_chain: data.key_chain,
accounts: vec![(data.identifier, account)],
},
);
}
}
}
@ -170,28 +190,71 @@ impl WalletChainStore {
pub fn insert_private_account_data(
&mut self,
account_id: nssa::AccountId,
identifier: nssa_core::Identifier,
account: nssa_core::account::Account,
) {
debug!("inserting at address {account_id}, this account {account:?}");
let entry = self
// Update default accounts if present
if let Entry::Occupied(mut entry) = self
.user_data
.default_user_private_accounts
.entry(account_id)
.and_modify(|data| data.1 = account.clone());
{
let entry = entry.get_mut();
if let Some((_, acc)) = entry.accounts.iter_mut().find(|(id, _)| *id == identifier) {
*acc = account;
} else {
entry.accounts.push((identifier, account));
}
return;
}
if matches!(entry, Entry::Vacant(_)) {
self.user_data
// Otherwise update the private key tree
// Find the node by iterating all tree nodes for this account_id
let chain_index = self
.user_data
.private_key_tree
.account_id_map
.get(&account_id)
.cloned();
if let Some(chain_index) = chain_index {
// Node already in account_id_map — update its entry
if let Some(node) = self
.user_data
.private_key_tree
.account_id_map
.get(&account_id)
.map(|chain_index| {
.key_map
.get_mut(&chain_index)
{
if let Some((_, acc)) = node.value.1.iter_mut().find(|(id, _)| *id == identifier) {
*acc = account;
} else {
node.value.1.push((identifier, account));
}
}
} else {
// Node not yet in account_id_map — find it by checking all nodes
for (ci, node) in &mut self.user_data.private_key_tree.key_map {
let expected_id =
nssa::AccountId::from((&node.value.0.nullifier_public_key, identifier));
if expected_id == account_id {
if let Some((_, acc)) =
node.value.1.iter_mut().find(|(id, _)| *id == identifier)
{
*acc = account;
} else {
node.value.1.push((identifier, account));
}
// Register in account_id_map
self.user_data
.private_key_tree
.key_map
.entry(chain_index.clone())
.and_modify(|data| data.value.1 = account)
});
.account_id_map
.insert(account_id, ci.clone());
break;
}
}
}
}
}
@ -199,7 +262,7 @@ impl WalletChainStore {
#[cfg(test)]
mod tests {
use key_protocol::key_management::key_tree::{
keys_private::ChildKeysPrivate, keys_public::ChildKeysPublic, traits::KeyNode as _,
keys_private::ChildKeysPrivate, keys_public::ChildKeysPublic,
};
use super::*;
@ -228,7 +291,7 @@ mod tests {
data: public_data,
}),
PersistentAccountData::Private(Box::new(PersistentAccountDataPrivate {
account_id: private_data.account_id(),
identifiers: vec![],
chain_index: ChainIndex::root(),
data: private_data,
})),

View File

@ -82,7 +82,8 @@ pub enum NewSubcommand {
/// Label to assign to the new account.
label: Option<String>,
},
/// Register new private account.
/// Single-account convenience: creates a key node and auto-registers one account with a random
/// identifier.
Private {
#[arg(long)]
/// Chain index of a parent node.
@ -91,6 +92,13 @@ pub enum NewSubcommand {
/// Label to assign to the new account.
label: Option<String>,
},
/// Recommended for receiving from multiple senders: creates a key node (npk + vpk) without
/// registering any account.
PrivateAccountsKey {
#[arg(long)]
/// Chain index of a parent node.
cci: Option<ChainIndex>,
},
}
impl WalletSubcommand for NewSubcommand {
@ -149,6 +157,15 @@ impl WalletSubcommand for NewSubcommand {
let (account_id, chain_index) = wallet_core.create_new_account_private(cci);
let node = wallet_core
.storage
.user_data
.private_key_tree
.key_map
.get(&chain_index)
.expect("Node was just inserted");
let key = &node.value.0;
if let Some(label) = label {
wallet_core
.storage
@ -156,14 +173,8 @@ impl WalletSubcommand for NewSubcommand {
.insert(account_id.to_string(), Label::new(label));
}
let (key, _) = wallet_core
.storage
.user_data
.get_private_account(account_id)
.unwrap();
println!(
"Generated new account with account_id Private/{account_id} at path {chain_index}",
"Generated new account with account_id Private/{account_id} at path {chain_index}"
);
println!("With npk {}", hex::encode(key.nullifier_public_key.0));
println!(
@ -175,6 +186,29 @@ impl WalletSubcommand for NewSubcommand {
Ok(SubcommandReturnValue::RegisterAccount { account_id })
}
Self::PrivateAccountsKey { cci } => {
let chain_index = wallet_core.create_private_accounts_key(cci);
let node = wallet_core
.storage
.user_data
.private_key_tree
.key_map
.get(&chain_index)
.expect("Node was just inserted");
let key = &node.value.0;
println!("Generated new private key node at path {chain_index}");
println!("With npk {}", hex::encode(key.nullifier_public_key.0));
println!(
"With vpk {}",
hex::encode(key.viewing_public_key.to_bytes())
);
wallet_core.store_persistent_data().await?;
Ok(SubcommandReturnValue::Empty)
}
}
}
}
@ -229,7 +263,7 @@ impl WalletSubcommand for AccountSubcommand {
println!("pk {}", hex::encode(public_key.value()));
}
AccountPrivacyKind::Private => {
let (key, _) = wallet_core
let (key, _, _) = wallet_core
.storage
.user_data
.get_private_account(account_id)
@ -272,21 +306,7 @@ impl WalletSubcommand for AccountSubcommand {
Self::New(new_subcommand) => new_subcommand.handle_subcommand(wallet_core).await,
Self::SyncPrivate => {
let curr_last_block = wallet_core.sequencer_client.get_last_block_id().await?;
if wallet_core
.storage
.user_data
.private_key_tree
.account_id_map
.is_empty()
{
wallet_core.last_synced_block = curr_last_block;
wallet_core.store_persistent_data().await?;
} else {
wallet_core.sync_to_block(curr_last_block).await?;
}
wallet_core.sync_to_block(curr_last_block).await?;
Ok(SubcommandReturnValue::SyncedToBlock(curr_last_block))
}
Self::List { long } => {

295
wallet/src/cli/group.rs Normal file
View File

@ -0,0 +1,295 @@
use anyhow::{Context as _, Result};
use clap::Subcommand;
use key_protocol::key_management::group_key_holder::GroupKeyHolder;
use nssa::AccountId;
use nssa_core::program::PdaSeed;
use crate::{
WalletCore,
cli::{SubcommandReturnValue, WalletSubcommand},
};
/// Group PDA management commands.
#[derive(Subcommand, Debug, Clone)]
pub enum GroupSubcommand {
/// Create a new group with a fresh random GMS.
New {
/// Human-readable name for the group.
name: String,
},
/// Import a group from raw GMS bytes.
Import {
/// Human-readable name for the group.
name: String,
/// Raw GMS as 64-character hex string.
#[arg(long)]
gms: String,
/// Epoch (defaults to 0).
#[arg(long, default_value = "0")]
epoch: u32,
},
/// Export the raw GMS hex for backup or manual distribution.
Export {
/// Group name.
name: String,
},
/// List all groups with their epochs.
#[command(visible_alias = "ls")]
List,
/// Derive keys for a PDA seed and show the resulting AccountId.
Derive {
/// Group name.
name: String,
/// PDA seed as 64-character hex string.
#[arg(long)]
seed: String,
/// Program ID as hex string (u32x8 little-endian).
#[arg(long)]
program_id: String,
},
/// Remove a group from the wallet.
Remove {
/// Group name.
name: String,
},
/// Seal the group's GMS for a recipient (invite).
Invite {
/// Group name.
name: String,
/// Recipient's viewing public key as hex string.
#[arg(long)]
vpk: String,
},
/// Unseal a received GMS and store it (join a group).
Join {
/// Human-readable name to store the group under.
name: String,
/// Sealed GMS as hex string (from the inviter).
#[arg(long)]
sealed: String,
/// Account label or Private/<id> whose VSK to use for decryption.
#[arg(long)]
account: String,
},
/// Ratchet the GMS to exclude removed members.
Ratchet {
/// Group name.
name: String,
},
}
impl WalletSubcommand for GroupSubcommand {
async fn handle_subcommand(
self,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
match self {
Self::New { name } => {
if wallet_core
.storage()
.user_data
.get_group_key_holder(&name)
.is_some()
{
anyhow::bail!("Group '{name}' already exists");
}
let holder = GroupKeyHolder::new();
wallet_core
.storage_mut()
.user_data
.insert_group_key_holder(name.clone(), holder);
wallet_core.store_persistent_data().await?;
println!("Created group '{name}' at epoch 0");
Ok(SubcommandReturnValue::Empty)
}
Self::Import { name, gms, epoch } => {
if wallet_core
.storage()
.user_data
.get_group_key_holder(&name)
.is_some()
{
anyhow::bail!("Group '{name}' already exists");
}
let gms_bytes: [u8; 32] = hex::decode(&gms)
.context("Invalid GMS hex")?
.try_into()
.map_err(|_| anyhow::anyhow!("GMS must be exactly 32 bytes"))?;
let holder = GroupKeyHolder::from_gms_and_epoch(gms_bytes, epoch);
wallet_core
.storage_mut()
.user_data
.insert_group_key_holder(name.clone(), holder);
wallet_core.store_persistent_data().await?;
println!("Imported group '{name}' at epoch {epoch}");
Ok(SubcommandReturnValue::Empty)
}
Self::Export { name } => {
let holder = wallet_core
.storage()
.user_data
.get_group_key_holder(&name)
.context(format!("Group '{name}' not found"))?;
let gms_hex = hex::encode(holder.dangerous_raw_gms());
let epoch = holder.epoch();
println!("Group: {name}");
println!("Epoch: {epoch}");
println!("GMS: {gms_hex}");
Ok(SubcommandReturnValue::Empty)
}
Self::List => {
let holders = &wallet_core.storage().user_data.group_key_holders;
if holders.is_empty() {
println!("No groups found");
} else {
for (name, holder) in holders {
println!("{name} (epoch {})", holder.epoch());
}
}
Ok(SubcommandReturnValue::Empty)
}
Self::Derive {
name,
seed,
program_id,
} => {
let holder = wallet_core
.storage()
.user_data
.get_group_key_holder(&name)
.context(format!("Group '{name}' not found"))?;
let seed_bytes: [u8; 32] = hex::decode(&seed)
.context("Invalid seed hex")?
.try_into()
.map_err(|_| anyhow::anyhow!("Seed must be exactly 32 bytes"))?;
let pda_seed = PdaSeed::new(seed_bytes);
let pid_bytes =
hex::decode(&program_id).context("Invalid program ID hex")?;
if pid_bytes.len() != 32 {
anyhow::bail!("Program ID must be exactly 32 bytes");
}
let mut pid: nssa_core::program::ProgramId = [0; 8];
for (i, chunk) in pid_bytes.chunks_exact(4).enumerate() {
pid[i] = u32::from_le_bytes(chunk.try_into().unwrap());
}
let keys = holder.derive_keys_for_pda(&pda_seed);
let npk = keys.generate_nullifier_public_key();
let vpk = keys.generate_viewing_public_key();
let account_id = AccountId::for_private_pda(&pid, &pda_seed, &npk);
println!("Group: {name}");
println!("NPK: {}", hex::encode(npk.0));
println!("VPK: {}", hex::encode(&vpk.0));
println!("AccountId: {account_id}");
Ok(SubcommandReturnValue::Empty)
}
Self::Remove { name } => {
if wallet_core
.storage_mut()
.user_data
.group_key_holders
.remove(&name)
.is_none()
{
anyhow::bail!("Group '{name}' not found");
}
wallet_core.store_persistent_data().await?;
println!("Removed group '{name}'");
Ok(SubcommandReturnValue::Empty)
}
Self::Invite { name, vpk } => {
let holder = wallet_core
.storage()
.user_data
.get_group_key_holder(&name)
.context(format!("Group '{name}' not found"))?;
let vpk_bytes = hex::decode(&vpk).context("Invalid VPK hex")?;
let recipient_vpk =
nssa_core::encryption::shared_key_derivation::Secp256k1Point(vpk_bytes);
let sealed = holder.seal_for(&recipient_vpk);
println!("{}", hex::encode(&sealed));
Ok(SubcommandReturnValue::Empty)
}
Self::Join {
name,
sealed,
account,
} => {
if wallet_core
.storage()
.user_data
.get_group_key_holder(&name)
.is_some()
{
anyhow::bail!("Group '{name}' already exists");
}
let sealed_bytes = hex::decode(&sealed).context("Invalid sealed hex")?;
// Resolve the account to get the VSK
let account_id: nssa::AccountId = account
.parse()
.context("Invalid account ID (use Private/<base58>)")?;
let (keychain, _) = wallet_core
.storage()
.user_data
.get_private_account(account_id)
.context("Private account not found")?;
let vsk = keychain.private_key_holder.viewing_secret_key;
let holder = GroupKeyHolder::unseal(&sealed_bytes, &vsk)
.map_err(|e| anyhow::anyhow!("Failed to unseal: {e:?}"))?;
let epoch = holder.epoch();
wallet_core
.storage_mut()
.user_data
.insert_group_key_holder(name.clone(), holder);
wallet_core.store_persistent_data().await?;
println!("Joined group '{name}' at epoch {epoch}");
Ok(SubcommandReturnValue::Empty)
}
Self::Ratchet { name } => {
let holder = wallet_core
.storage_mut()
.user_data
.group_key_holders
.get_mut(&name)
.context(format!("Group '{name}' not found"))?;
let mut salt = [0_u8; 32];
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut salt);
holder.ratchet(salt);
let epoch = holder.epoch();
wallet_core.store_persistent_data().await?;
println!("Ratcheted group '{name}' to epoch {epoch}");
println!("Re-invite remaining members with 'group invite'");
Ok(SubcommandReturnValue::Empty)
}
}
}
}

View File

@ -59,6 +59,10 @@ pub enum AuthTransferSubcommand {
/// `to_vpk` - valid 33 byte hex string.
#[arg(long)]
to_vpk: Option<String>,
/// Identifier for the recipient's private account (only used when sending to a foreign
/// private account via `--to-npk`/`--to-vpk`).
#[arg(long)]
to_identifier: Option<u128>,
/// amount - amount of balance to move.
#[arg(long)]
amount: u128,
@ -132,6 +136,7 @@ impl WalletSubcommand for AuthTransferSubcommand {
to_label,
to_npk,
to_vpk,
to_identifier,
amount,
} => {
let from = resolve_id_or_label(
@ -210,6 +215,7 @@ impl WalletSubcommand for AuthTransferSubcommand {
from,
to_npk,
to_vpk,
to_identifier,
amount,
},
)
@ -220,6 +226,7 @@ impl WalletSubcommand for AuthTransferSubcommand {
from,
to_npk,
to_vpk,
to_identifier,
amount,
},
)
@ -304,6 +311,9 @@ pub enum NativeTokenTransferProgramSubcommandShielded {
/// `to_vpk` - valid 33 byte hex string.
#[arg(long)]
to_vpk: String,
/// Identifier for the recipient's private account.
#[arg(long)]
to_identifier: Option<u128>,
/// amount - amount of balance to move.
#[arg(long)]
amount: u128,
@ -341,6 +351,9 @@ pub enum NativeTokenTransferProgramSubcommandPrivate {
/// `to_vpk` - valid 33 byte hex string.
#[arg(long)]
to_vpk: String,
/// Identifier for the recipient's private account.
#[arg(long)]
to_identifier: Option<u128>,
/// amount - amount of balance to move.
#[arg(long)]
amount: u128,
@ -382,6 +395,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandPrivate {
from,
to_npk,
to_vpk,
to_identifier,
amount,
} => {
let from: AccountId = from.parse().unwrap();
@ -397,7 +411,13 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandPrivate {
nssa_core::encryption::shared_key_derivation::Secp256k1Point(to_vpk.to_vec());
let (tx_hash, [secret_from, _]) = NativeTokenTransfer(wallet_core)
.send_private_transfer_to_outer_account(from, to_npk, to_vpk, amount)
.send_private_transfer_to_outer_account(
from,
to_npk,
to_vpk,
to_identifier.unwrap_or_else(rand::random),
amount,
)
.await?;
println!("Transaction hash is {tx_hash}");
@ -456,6 +476,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded {
from,
to_npk,
to_vpk,
to_identifier,
amount,
} => {
let from: AccountId = from.parse().unwrap();
@ -472,7 +493,13 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded {
nssa_core::encryption::shared_key_derivation::Secp256k1Point(to_vpk.to_vec());
let (tx_hash, _) = NativeTokenTransfer(wallet_core)
.send_shielded_transfer_to_outer_account(from, to_npk, to_vpk, amount)
.send_shielded_transfer_to_outer_account(
from,
to_npk,
to_vpk,
to_identifier.unwrap_or_else(rand::random),
amount,
)
.await?;
println!("Transaction hash is {tx_hash}");

View File

@ -73,6 +73,10 @@ pub enum TokenProgramAgnosticSubcommand {
/// `to_vpk` - valid 33 byte hex string.
#[arg(long)]
to_vpk: Option<String>,
/// Identifier for the recipient's private account (only used when sending to a foreign
/// private account via `--to-npk`/`--to-vpk`).
#[arg(long)]
to_identifier: Option<u128>,
/// amount - amount of balance to move.
#[arg(long)]
amount: u128,
@ -139,6 +143,10 @@ pub enum TokenProgramAgnosticSubcommand {
/// `to_vpk` - valid 33 byte hex string.
#[arg(long)]
holder_vpk: Option<String>,
/// Identifier for the holder's private account (only used when minting to a foreign
/// private account via `--holder-npk`/`--holder-vpk`).
#[arg(long)]
holder_identifier: Option<u128>,
/// amount - amount of balance to mint.
#[arg(long)]
amount: u128,
@ -228,6 +236,7 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand {
to_label,
to_npk,
to_vpk,
to_identifier,
amount,
} => {
let from = resolve_id_or_label(
@ -313,6 +322,7 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand {
sender_account_id: from,
recipient_npk: to_npk,
recipient_vpk: to_vpk,
recipient_identifier: to_identifier,
balance_to_move: amount,
},
),
@ -321,6 +331,7 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand {
sender_account_id: from,
recipient_npk: to_npk,
recipient_vpk: to_vpk,
recipient_identifier: to_identifier,
balance_to_move: amount,
},
),
@ -403,6 +414,7 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand {
holder_label,
holder_npk,
holder_vpk,
holder_identifier,
amount,
} => {
let definition = resolve_id_or_label(
@ -490,6 +502,7 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand {
definition_account_id: definition,
holder_npk,
holder_vpk,
holder_identifier,
amount,
},
),
@ -498,6 +511,7 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand {
definition_account_id: definition,
holder_npk,
holder_vpk,
holder_identifier,
amount,
},
),
@ -585,6 +599,9 @@ pub enum TokenProgramSubcommandPrivate {
/// `recipient_vpk` - valid 33 byte hex string.
#[arg(long)]
recipient_vpk: String,
/// Identifier for the recipient's private account.
#[arg(long)]
recipient_identifier: Option<u128>,
#[arg(short, long)]
balance_to_move: u128,
},
@ -614,6 +631,9 @@ pub enum TokenProgramSubcommandPrivate {
holder_npk: String,
#[arg(short, long)]
holder_vpk: String,
/// Identifier for the holder's private account.
#[arg(long)]
holder_identifier: Option<u128>,
#[arg(short, long)]
amount: u128,
},
@ -673,6 +693,9 @@ pub enum TokenProgramSubcommandShielded {
/// `recipient_vpk` - valid 33 byte hex string.
#[arg(long)]
recipient_vpk: String,
/// Identifier for the recipient's private account.
#[arg(long)]
recipient_identifier: Option<u128>,
#[arg(short, long)]
balance_to_move: u128,
},
@ -702,6 +725,9 @@ pub enum TokenProgramSubcommandShielded {
holder_npk: String,
#[arg(short, long)]
holder_vpk: String,
/// Identifier for the holder's private account.
#[arg(long)]
holder_identifier: Option<u128>,
#[arg(short, long)]
amount: u128,
},
@ -862,6 +888,7 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate {
sender_account_id,
recipient_npk,
recipient_vpk,
recipient_identifier,
balance_to_move,
} => {
let sender_account_id: AccountId = sender_account_id.parse().unwrap();
@ -882,6 +909,7 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate {
sender_account_id,
recipient_npk,
recipient_vpk,
recipient_identifier.unwrap_or_else(rand::random),
balance_to_move,
)
.await?;
@ -979,6 +1007,7 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate {
definition_account_id,
holder_npk,
holder_vpk,
holder_identifier,
amount,
} => {
let definition_account_id: AccountId = definition_account_id.parse().unwrap();
@ -1000,6 +1029,7 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate {
definition_account_id,
holder_npk,
holder_vpk,
holder_identifier.unwrap_or_else(rand::random),
amount,
)
.await?;
@ -1144,6 +1174,7 @@ impl WalletSubcommand for TokenProgramSubcommandShielded {
sender_account_id,
recipient_npk,
recipient_vpk,
recipient_identifier,
balance_to_move,
} => {
let sender_account_id: AccountId = sender_account_id.parse().unwrap();
@ -1164,6 +1195,7 @@ impl WalletSubcommand for TokenProgramSubcommandShielded {
sender_account_id,
recipient_npk,
recipient_vpk,
recipient_identifier.unwrap_or_else(rand::random),
balance_to_move,
)
.await?;
@ -1283,6 +1315,7 @@ impl WalletSubcommand for TokenProgramSubcommandShielded {
definition_account_id,
holder_npk,
holder_vpk,
holder_identifier,
amount,
} => {
let definition_account_id: AccountId = definition_account_id.parse().unwrap();
@ -1304,6 +1337,7 @@ impl WalletSubcommand for TokenProgramSubcommandShielded {
definition_account_id,
holder_npk,
holder_vpk,
holder_identifier.unwrap_or_else(rand::random),
amount,
)
.await?;

View File

@ -28,7 +28,7 @@ pub struct PersistentAccountDataPublic {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PersistentAccountDataPrivate {
pub account_id: nssa::AccountId,
pub identifiers: Vec<nssa_core::Identifier>,
pub chain_index: ChainIndex,
pub data: ChildKeysPrivate,
}
@ -44,10 +44,10 @@ pub enum InitialAccountData {
impl InitialAccountData {
#[must_use]
pub const fn account_id(&self) -> nssa::AccountId {
pub fn account_id(&self) -> nssa::AccountId {
match &self {
Self::Public(acc) => acc.account_id,
Self::Private(acc) => acc.account_id,
Self::Private(acc) => acc.account_id(),
}
}
@ -123,17 +123,6 @@ impl PersistentStorage {
}
}
impl PersistentAccountData {
#[must_use]
pub fn account_id(&self) -> nssa::AccountId {
match &self {
Self::Public(acc) => acc.account_id,
Self::Private(acc) => acc.account_id,
Self::Preconfigured(acc) => acc.account_id(),
}
}
}
impl From<PublicAccountPrivateInitialData> for InitialAccountData {
fn from(value: PublicAccountPrivateInitialData) -> Self {
Self::Public(value)

View File

@ -165,17 +165,16 @@ pub fn produce_data_for_storage(
}
}
for (account_id, key) in &user_data.private_key_tree.account_id_map {
if let Some(data) = user_data.private_key_tree.key_map.get(key) {
vec_for_storage.push(
PersistentAccountDataPrivate {
account_id: *account_id,
chain_index: key.clone(),
data: data.clone(),
}
.into(),
);
}
for (chain_index, node) in &user_data.private_key_tree.key_map {
let identifiers = node.value.1.iter().map(|(id, _)| *id).collect();
vec_for_storage.push(
PersistentAccountDataPrivate {
identifiers,
chain_index: chain_index.clone(),
data: node.clone(),
}
.into(),
);
}
for (account_id, key) in &user_data.default_pub_account_signing_keys {
@ -188,15 +187,17 @@ pub fn produce_data_for_storage(
);
}
for (account_id, (key_chain, account)) in &user_data.default_user_private_accounts {
vec_for_storage.push(
InitialAccountData::Private(Box::new(PrivateAccountPrivateInitialData {
account_id: *account_id,
account: account.clone(),
key_chain: key_chain.clone(),
}))
.into(),
);
for entry in user_data.default_user_private_accounts.values() {
for (identifier, account) in &entry.accounts {
vec_for_storage.push(
InitialAccountData::Private(Box::new(PrivateAccountPrivateInitialData {
account: account.clone(),
key_chain: entry.key_chain.clone(),
identifier: *identifier,
}))
.into(),
);
}
}
PersistentStorage {

View File

@ -15,7 +15,7 @@ use bip39::Mnemonic;
use chain_storage::WalletChainStore;
use common::{HashType, transaction::NSSATransaction};
use config::WalletConfig;
use key_protocol::key_management::key_tree::{chain_index::ChainIndex, traits::KeyNode as _};
use key_protocol::key_management::key_tree::chain_index::ChainIndex;
use log::info;
use nssa::{
Account, AccountId, PrivacyPreservingTransaction,
@ -256,13 +256,35 @@ impl WalletCore {
.generate_new_public_transaction_private_key(chain_index)
}
pub fn create_private_accounts_key(&mut self, chain_index: Option<ChainIndex>) -> ChainIndex {
self.storage
.user_data
.create_private_accounts_key(chain_index)
}
pub fn create_new_account_private(
&mut self,
chain_index: Option<ChainIndex>,
) -> (AccountId, ChainIndex) {
self.storage
let cci = self
.storage
.user_data
.generate_new_privacy_preserving_transaction_key_chain(chain_index)
.create_private_accounts_key(chain_index);
let identifier: nssa_core::Identifier = rand::random();
let npk = self
.storage
.user_data
.private_key_tree
.key_map
.get(&cci)
.expect("Node was just inserted")
.value
.0
.nullifier_public_key;
let account_id = AccountId::from((&npk, identifier));
self.storage
.insert_private_account_data(account_id, identifier, Account::default());
(account_id, cci)
}
/// Get account balance.
@ -295,13 +317,14 @@ impl WalletCore {
self.storage
.user_data
.get_private_account(account_id)
.map(|value| value.1.clone())
.map(|(_keys, account, _identifier)| account)
}
#[must_use]
pub fn get_private_account_commitment(&self, account_id: AccountId) -> Option<Commitment> {
let (keys, account) = self.storage.user_data.get_private_account(account_id)?;
Some(Commitment::new(&keys.nullifier_public_key, account))
let (_keys, account, _identifier) =
self.storage.user_data.get_private_account(account_id)?;
Some(Commitment::new(&account_id, &account))
}
/// Poll transactions.
@ -334,7 +357,7 @@ impl WalletCore {
let acc_ead = tx.message.encrypted_private_post_states[output_index].clone();
let acc_comm = tx.message.new_commitments[output_index].clone();
let res_acc = nssa_core::EncryptionScheme::decrypt(
let (identifier, res_acc) = nssa_core::EncryptionScheme::decrypt(
&acc_ead.ciphertext,
secret,
&acc_comm,
@ -347,7 +370,7 @@ impl WalletCore {
println!("Received new acc {res_acc:#?}");
self.storage
.insert_private_account_data(*acc_account_id, res_acc);
.insert_private_account_data(*acc_account_id, identifier, res_acc);
}
AccDecodeData::Skip => {}
}
@ -393,7 +416,7 @@ impl WalletCore {
acc_manager.visibility_mask().to_vec(),
private_account_keys
.iter()
.map(|keys| (keys.npk, keys.ssk))
.map(|keys| (keys.npk, keys.identifier, keys.ssk))
.collect::<Vec<_>>(),
acc_manager.private_account_auth(),
acc_manager.private_account_membership_proofs(),
@ -483,33 +506,33 @@ impl WalletCore {
.storage
.user_data
.default_user_private_accounts
.iter()
.map(|(acc_account_id, (key_chain, _))| (*acc_account_id, key_chain, None))
.chain(self.storage.user_data.private_key_tree.key_map.iter().map(
|(chain_index, keys_node)| {
(
keys_node.account_id(),
&keys_node.value.0,
chain_index.index(),
)
},
));
.values()
.map(|entry| (&entry.key_chain, None))
.chain(
self.storage
.user_data
.private_key_tree
.key_map
.iter()
.map(|(chain_index, keys_node)| (&keys_node.value.0, chain_index.index())),
);
let affected_accounts = private_account_key_chains
.flat_map(|(acc_account_id, key_chain, index)| {
.flat_map(|(key_chain, index)| {
let view_tag = EncryptedAccountData::compute_view_tag(
&key_chain.nullifier_public_key,
&key_chain.viewing_public_key,
);
let new_commitments = &tx.message.new_commitments;
tx.message()
.encrypted_private_post_states
.iter()
.enumerate()
.filter(move |(_, encrypted_data)| encrypted_data.view_tag == view_tag)
.filter_map(|(ciph_id, encrypted_data)| {
.filter_map(move |(ciph_id, encrypted_data)| {
let ciphertext = &encrypted_data.ciphertext;
let commitment = &tx.message.new_commitments[ciph_id];
let commitment = &new_commitments[ciph_id];
let shared_secret =
key_chain.calculate_shared_secret_receiver(&encrypted_data.epk, index);
@ -521,18 +544,24 @@ impl WalletCore {
.try_into()
.expect("Ciphertext ID is expected to fit in u32"),
)
.map(|(identifier, res_acc)| {
let account_id = nssa::AccountId::from((
&key_chain.nullifier_public_key,
identifier,
));
(account_id, identifier, res_acc)
})
})
.map(move |res_acc| (acc_account_id, res_acc))
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
for (affected_account_id, new_acc) in affected_accounts {
for (affected_account_id, identifier, new_acc) in affected_accounts {
info!(
"Received new account for account_id {affected_account_id:#?} with account object {new_acc:#?}"
);
self.storage
.insert_private_account_data(affected_account_id, new_acc);
.insert_private_account_data(affected_account_id, identifier, new_acc);
}
}

View File

@ -2,7 +2,7 @@ use anyhow::Result;
use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder;
use nssa::{AccountId, PrivateKey};
use nssa_core::{
MembershipProof, NullifierPublicKey, NullifierSecretKey, SharedSecretKey,
Identifier, MembershipProof, NullifierPublicKey, NullifierSecretKey, SharedSecretKey,
account::{AccountWithMetadata, Nonce},
encryption::{EphemeralPublicKey, ViewingPublicKey},
program::{PdaSeed, ProgramId},
@ -17,6 +17,7 @@ pub enum PrivacyPreservingAccount {
PrivateForeign {
npk: NullifierPublicKey,
vpk: ViewingPublicKey,
identifier: Identifier,
},
/// A private PDA with externally-provided keys. The caller resolves the keys
/// (e.g. via `GroupKeyHolder::derive_keys_for_pda`) before constructing this variant.
@ -41,7 +42,11 @@ impl PrivacyPreservingAccount {
matches!(
&self,
Self::PrivateOwned(_)
| Self::PrivateForeign { npk: _, vpk: _ }
| Self::PrivateForeign {
npk: _,
vpk: _,
identifier: _,
}
| Self::PrivatePda { .. }
)
}
@ -49,6 +54,7 @@ impl PrivacyPreservingAccount {
pub struct PrivateAccountKeys {
pub npk: NullifierPublicKey,
pub identifier: Identifier,
pub ssk: SharedSecretKey,
pub vpk: ViewingPublicKey,
pub epk: EphemeralPublicKey,
@ -94,12 +100,17 @@ impl AccountManager {
(State::Private(pre), mask)
}
PrivacyPreservingAccount::PrivateForeign { npk, vpk } => {
PrivacyPreservingAccount::PrivateForeign {
npk,
vpk,
identifier,
} => {
let acc = nssa_core::account::Account::default();
let auth_acc = AccountWithMetadata::new(acc, false, &npk);
let auth_acc = AccountWithMetadata::new(acc, false, (&npk, identifier));
let pre = AccountPreparedData {
nsk: None,
npk,
identifier,
vpk,
pre_state: auth_acc,
proof: None,
@ -168,6 +179,7 @@ impl AccountManager {
Some(PrivateAccountKeys {
npk: pre.npk,
identifier: pre.identifier,
ssk: eph_holder.calculate_shared_secret_sender(&pre.vpk),
vpk: pre.vpk.clone(),
epk: eph_holder.generate_ephemeral_public_key(),
@ -226,6 +238,7 @@ impl AccountManager {
struct AccountPreparedData {
nsk: Option<NullifierSecretKey>,
npk: NullifierPublicKey,
identifier: Identifier,
vpk: ViewingPublicKey,
pre_state: AccountWithMetadata,
proof: Option<MembershipProof>,
@ -271,6 +284,7 @@ async fn private_pda_preparation(
Ok(AccountPreparedData {
nsk: exists.then_some(nsk),
npk,
identifier: 0,
vpk,
pre_state,
proof,
@ -281,11 +295,8 @@ async fn private_acc_preparation(
wallet: &WalletCore,
account_id: AccountId,
) -> Result<AccountPreparedData, ExecutionFailureKind> {
let Some((from_keys, from_acc)) = wallet
.storage
.user_data
.get_private_account(account_id)
.cloned()
let Some((from_keys, from_acc, from_identifier)) =
wallet.storage.user_data.get_private_account(account_id)
else {
return Err(ExecutionFailureKind::KeyNotFoundError);
};
@ -303,11 +314,12 @@ async fn private_acc_preparation(
// TODO: Technically we could allow unauthorized owned accounts, but currently we don't have
// support from that in the wallet.
let sender_pre = AccountWithMetadata::new(from_acc.clone(), true, &from_npk);
let sender_pre = AccountWithMetadata::new(from_acc.clone(), true, (&from_npk, from_identifier));
Ok(AccountPreparedData {
nsk: Some(nsk),
npk: from_npk,
identifier: from_identifier,
vpk: from_vpk,
pre_state: sender_pre,
proof,

View File

@ -2,7 +2,7 @@ use std::vec;
use common::HashType;
use nssa::{AccountId, program::Program};
use nssa_core::{NullifierPublicKey, SharedSecretKey, encryption::ViewingPublicKey};
use nssa_core::{Identifier, NullifierPublicKey, SharedSecretKey, encryption::ViewingPublicKey};
use super::{NativeTokenTransfer, auth_transfer_preparation};
use crate::{ExecutionFailureKind, PrivacyPreservingAccount};
@ -33,6 +33,7 @@ impl NativeTokenTransfer<'_> {
from: AccountId,
to_npk: NullifierPublicKey,
to_vpk: ViewingPublicKey,
to_identifier: Identifier,
balance_to_move: u128,
) -> Result<(HashType, [SharedSecretKey; 2]), ExecutionFailureKind> {
let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move);
@ -44,6 +45,7 @@ impl NativeTokenTransfer<'_> {
PrivacyPreservingAccount::PrivateForeign {
npk: to_npk,
vpk: to_vpk,
identifier: to_identifier,
},
],
instruction_data,

View File

@ -1,6 +1,6 @@
use common::HashType;
use nssa::AccountId;
use nssa_core::{NullifierPublicKey, SharedSecretKey, encryption::ViewingPublicKey};
use nssa_core::{Identifier, NullifierPublicKey, SharedSecretKey, encryption::ViewingPublicKey};
use super::{NativeTokenTransfer, auth_transfer_preparation};
use crate::{ExecutionFailureKind, PrivacyPreservingAccount};
@ -39,6 +39,7 @@ impl NativeTokenTransfer<'_> {
from: AccountId,
to_npk: NullifierPublicKey,
to_vpk: ViewingPublicKey,
to_identifier: Identifier,
balance_to_move: u128,
) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> {
let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move);
@ -50,6 +51,7 @@ impl NativeTokenTransfer<'_> {
PrivacyPreservingAccount::PrivateForeign {
npk: to_npk,
vpk: to_vpk,
identifier: to_identifier,
},
],
instruction_data,

View File

@ -1,6 +1,6 @@
use common::{HashType, transaction::NSSATransaction};
use nssa::{AccountId, program::Program};
use nssa_core::{NullifierPublicKey, SharedSecretKey, encryption::ViewingPublicKey};
use nssa_core::{Identifier, NullifierPublicKey, SharedSecretKey, encryption::ViewingPublicKey};
use sequencer_service_rpc::RpcClient as _;
use token_core::Instruction;
@ -247,6 +247,7 @@ impl Token<'_> {
sender_account_id: AccountId,
recipient_npk: NullifierPublicKey,
recipient_vpk: ViewingPublicKey,
recipient_identifier: Identifier,
amount: u128,
) -> Result<(HashType, [SharedSecretKey; 2]), ExecutionFailureKind> {
let instruction = Instruction::Transfer {
@ -262,6 +263,7 @@ impl Token<'_> {
PrivacyPreservingAccount::PrivateForeign {
npk: recipient_npk,
vpk: recipient_vpk,
identifier: recipient_identifier,
},
],
instruction_data,
@ -343,6 +345,7 @@ impl Token<'_> {
sender_account_id: AccountId,
recipient_npk: NullifierPublicKey,
recipient_vpk: ViewingPublicKey,
recipient_identifier: Identifier,
amount: u128,
) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> {
let instruction = Instruction::Transfer {
@ -358,6 +361,7 @@ impl Token<'_> {
PrivacyPreservingAccount::PrivateForeign {
npk: recipient_npk,
vpk: recipient_vpk,
identifier: recipient_identifier,
},
],
instruction_data,
@ -606,6 +610,7 @@ impl Token<'_> {
definition_account_id: AccountId,
holder_npk: NullifierPublicKey,
holder_vpk: ViewingPublicKey,
holder_identifier: Identifier,
amount: u128,
) -> Result<(HashType, [SharedSecretKey; 2]), ExecutionFailureKind> {
let instruction = Instruction::Mint {
@ -621,6 +626,7 @@ impl Token<'_> {
PrivacyPreservingAccount::PrivateForeign {
npk: holder_npk,
vpk: holder_vpk,
identifier: holder_identifier,
},
],
instruction_data,
@ -702,6 +708,7 @@ impl Token<'_> {
definition_account_id: AccountId,
holder_npk: NullifierPublicKey,
holder_vpk: ViewingPublicKey,
holder_identifier: Identifier,
amount: u128,
) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> {
let instruction = Instruction::Mint {
@ -717,6 +724,7 @@ impl Token<'_> {
PrivacyPreservingAccount::PrivateForeign {
npk: holder_npk,
vpk: holder_vpk,
identifier: holder_identifier,
},
],
instruction_data,