diff --git a/artifacts/program_methods/amm.bin b/artifacts/program_methods/amm.bin index a991cd7d..f23a72a7 100644 Binary files a/artifacts/program_methods/amm.bin and b/artifacts/program_methods/amm.bin differ diff --git a/artifacts/program_methods/associated_token_account.bin b/artifacts/program_methods/associated_token_account.bin index 3c708347..f1d7960b 100644 Binary files a/artifacts/program_methods/associated_token_account.bin and b/artifacts/program_methods/associated_token_account.bin differ diff --git a/artifacts/program_methods/authenticated_transfer.bin b/artifacts/program_methods/authenticated_transfer.bin index 87439bd2..e18985fb 100644 Binary files a/artifacts/program_methods/authenticated_transfer.bin and b/artifacts/program_methods/authenticated_transfer.bin differ diff --git a/artifacts/program_methods/clock.bin b/artifacts/program_methods/clock.bin index ba567c89..0196a5a2 100644 Binary files a/artifacts/program_methods/clock.bin and b/artifacts/program_methods/clock.bin differ diff --git a/artifacts/program_methods/pinata.bin b/artifacts/program_methods/pinata.bin index bdacaaaa..36631e40 100644 Binary files a/artifacts/program_methods/pinata.bin and b/artifacts/program_methods/pinata.bin differ diff --git a/artifacts/program_methods/pinata_token.bin b/artifacts/program_methods/pinata_token.bin index dd2e56e1..31210a8a 100644 Binary files a/artifacts/program_methods/pinata_token.bin and b/artifacts/program_methods/pinata_token.bin differ diff --git a/artifacts/program_methods/privacy_preserving_circuit.bin b/artifacts/program_methods/privacy_preserving_circuit.bin index da4e2652..cb18e5f0 100644 Binary files a/artifacts/program_methods/privacy_preserving_circuit.bin and b/artifacts/program_methods/privacy_preserving_circuit.bin differ diff --git a/artifacts/program_methods/token.bin b/artifacts/program_methods/token.bin index cd9bf03e..dc5d69ce 100644 Binary files a/artifacts/program_methods/token.bin and b/artifacts/program_methods/token.bin differ diff --git a/artifacts/test_program_methods/auth_asserting_noop.bin b/artifacts/test_program_methods/auth_asserting_noop.bin index 4f875689..8b36b2bc 100644 Binary files a/artifacts/test_program_methods/auth_asserting_noop.bin and b/artifacts/test_program_methods/auth_asserting_noop.bin differ diff --git a/artifacts/test_program_methods/burner.bin b/artifacts/test_program_methods/burner.bin index a31debb6..b3baa0b1 100644 Binary files a/artifacts/test_program_methods/burner.bin and b/artifacts/test_program_methods/burner.bin differ diff --git a/artifacts/test_program_methods/chain_caller.bin b/artifacts/test_program_methods/chain_caller.bin index 42247274..4a95caae 100644 Binary files a/artifacts/test_program_methods/chain_caller.bin and b/artifacts/test_program_methods/chain_caller.bin differ diff --git a/artifacts/test_program_methods/changer_claimer.bin b/artifacts/test_program_methods/changer_claimer.bin index 9cef121c..34101f0f 100644 Binary files a/artifacts/test_program_methods/changer_claimer.bin and b/artifacts/test_program_methods/changer_claimer.bin differ diff --git a/artifacts/test_program_methods/claimer.bin b/artifacts/test_program_methods/claimer.bin index 67e8d6b2..3ababa7e 100644 Binary files a/artifacts/test_program_methods/claimer.bin and b/artifacts/test_program_methods/claimer.bin differ diff --git a/artifacts/test_program_methods/clock_chain_caller.bin b/artifacts/test_program_methods/clock_chain_caller.bin index 112071cd..f426bebb 100644 Binary files a/artifacts/test_program_methods/clock_chain_caller.bin and b/artifacts/test_program_methods/clock_chain_caller.bin differ diff --git a/artifacts/test_program_methods/data_changer.bin b/artifacts/test_program_methods/data_changer.bin index a69cf9ce..847d9448 100644 Binary files a/artifacts/test_program_methods/data_changer.bin and b/artifacts/test_program_methods/data_changer.bin differ diff --git a/artifacts/test_program_methods/extra_output.bin b/artifacts/test_program_methods/extra_output.bin index 52e1e6cf..8081a5a3 100644 Binary files a/artifacts/test_program_methods/extra_output.bin and b/artifacts/test_program_methods/extra_output.bin differ diff --git a/artifacts/test_program_methods/flash_swap_callback.bin b/artifacts/test_program_methods/flash_swap_callback.bin index 91738b4a..7d337106 100644 Binary files a/artifacts/test_program_methods/flash_swap_callback.bin and b/artifacts/test_program_methods/flash_swap_callback.bin differ diff --git a/artifacts/test_program_methods/flash_swap_initiator.bin b/artifacts/test_program_methods/flash_swap_initiator.bin index 8c1c48d5..ea1ce2ea 100644 Binary files a/artifacts/test_program_methods/flash_swap_initiator.bin and b/artifacts/test_program_methods/flash_swap_initiator.bin differ diff --git a/artifacts/test_program_methods/malicious_authorization_changer.bin b/artifacts/test_program_methods/malicious_authorization_changer.bin index 51ac1c14..0c7ceaad 100644 Binary files a/artifacts/test_program_methods/malicious_authorization_changer.bin and b/artifacts/test_program_methods/malicious_authorization_changer.bin differ diff --git a/artifacts/test_program_methods/malicious_caller_program_id.bin b/artifacts/test_program_methods/malicious_caller_program_id.bin index 0ee8cb70..6b37d11e 100644 Binary files a/artifacts/test_program_methods/malicious_caller_program_id.bin and b/artifacts/test_program_methods/malicious_caller_program_id.bin differ diff --git a/artifacts/test_program_methods/malicious_self_program_id.bin b/artifacts/test_program_methods/malicious_self_program_id.bin index 5e9bfc3d..45705bea 100644 Binary files a/artifacts/test_program_methods/malicious_self_program_id.bin and b/artifacts/test_program_methods/malicious_self_program_id.bin differ diff --git a/artifacts/test_program_methods/minter.bin b/artifacts/test_program_methods/minter.bin index a87b7a31..328650c5 100644 Binary files a/artifacts/test_program_methods/minter.bin and b/artifacts/test_program_methods/minter.bin differ diff --git a/artifacts/test_program_methods/missing_output.bin b/artifacts/test_program_methods/missing_output.bin index efadb9fe..f6deddbd 100644 Binary files a/artifacts/test_program_methods/missing_output.bin and b/artifacts/test_program_methods/missing_output.bin differ diff --git a/artifacts/test_program_methods/modified_transfer.bin b/artifacts/test_program_methods/modified_transfer.bin index 184fc2c6..f59d957e 100644 Binary files a/artifacts/test_program_methods/modified_transfer.bin and b/artifacts/test_program_methods/modified_transfer.bin differ diff --git a/artifacts/test_program_methods/nonce_changer.bin b/artifacts/test_program_methods/nonce_changer.bin index 383f9eec..f6093b02 100644 Binary files a/artifacts/test_program_methods/nonce_changer.bin and b/artifacts/test_program_methods/nonce_changer.bin differ diff --git a/artifacts/test_program_methods/noop.bin b/artifacts/test_program_methods/noop.bin index cf431771..a82b4282 100644 Binary files a/artifacts/test_program_methods/noop.bin and b/artifacts/test_program_methods/noop.bin differ diff --git a/artifacts/test_program_methods/pda_claimer.bin b/artifacts/test_program_methods/pda_claimer.bin index d113586f..c7d3fffc 100644 Binary files a/artifacts/test_program_methods/pda_claimer.bin and b/artifacts/test_program_methods/pda_claimer.bin differ diff --git a/artifacts/test_program_methods/pinata_cooldown.bin b/artifacts/test_program_methods/pinata_cooldown.bin index cc229c51..44d0d1bb 100644 Binary files a/artifacts/test_program_methods/pinata_cooldown.bin and b/artifacts/test_program_methods/pinata_cooldown.bin differ diff --git a/artifacts/test_program_methods/private_pda_delegator.bin b/artifacts/test_program_methods/private_pda_delegator.bin index 1bd9b65a..5078d317 100644 Binary files a/artifacts/test_program_methods/private_pda_delegator.bin and b/artifacts/test_program_methods/private_pda_delegator.bin differ diff --git a/artifacts/test_program_methods/program_owner_changer.bin b/artifacts/test_program_methods/program_owner_changer.bin index b81e33cb..910544e6 100644 Binary files a/artifacts/test_program_methods/program_owner_changer.bin and b/artifacts/test_program_methods/program_owner_changer.bin differ diff --git a/artifacts/test_program_methods/simple_balance_transfer.bin b/artifacts/test_program_methods/simple_balance_transfer.bin index ecdb2f7b..f7af292e 100644 Binary files a/artifacts/test_program_methods/simple_balance_transfer.bin and b/artifacts/test_program_methods/simple_balance_transfer.bin differ diff --git a/artifacts/test_program_methods/time_locked_transfer.bin b/artifacts/test_program_methods/time_locked_transfer.bin index 5e35f996..c2c7ee3a 100644 Binary files a/artifacts/test_program_methods/time_locked_transfer.bin and b/artifacts/test_program_methods/time_locked_transfer.bin differ diff --git a/artifacts/test_program_methods/two_pda_claimer.bin b/artifacts/test_program_methods/two_pda_claimer.bin index f58a3378..b79278d2 100644 Binary files a/artifacts/test_program_methods/two_pda_claimer.bin and b/artifacts/test_program_methods/two_pda_claimer.bin differ diff --git a/artifacts/test_program_methods/validity_window.bin b/artifacts/test_program_methods/validity_window.bin index 0507940d..6311e18d 100644 Binary files a/artifacts/test_program_methods/validity_window.bin and b/artifacts/test_program_methods/validity_window.bin differ diff --git a/artifacts/test_program_methods/validity_window_chain_caller.bin b/artifacts/test_program_methods/validity_window_chain_caller.bin index c1db5479..b428ab45 100644 Binary files a/artifacts/test_program_methods/validity_window_chain_caller.bin and b/artifacts/test_program_methods/validity_window_chain_caller.bin differ diff --git a/completions/README.md b/completions/README.md index d274774c..b12f1823 100644 --- a/completions/README.md +++ b/completions/README.md @@ -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 2. Rebuild the completion cache: ```sh - rm -f ~/.zcompdump* - exec zsh + rm -rf ~/.zcompdump* && exec zsh ``` ### Account IDs not completing diff --git a/completions/bash/wallet b/completions/bash/wallet index b01e5607..a4d390f6 100644 --- a/completions/bash/wallet +++ b/completions/bash/wallet @@ -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 "") diff --git a/completions/zsh/_wallet b/completions/zsh/_wallet index 2d4fe26b..8f573ab0 100644 --- a/completions/zsh/_wallet +++ b/completions/zsh/_wallet @@ -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 "$@" diff --git a/docs/LEZ testnet v0.1 tutorials/token-transfer.md b/docs/LEZ testnet v0.1 tutorials/token-transfer.md index 156f0b1f..3a1ef43f 100644 --- a/docs/LEZ testnet v0.1 tutorials/token-transfer.md +++ b/docs/LEZ testnet v0.1 tutorials/token-transfer.md @@ -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 owner’s 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] -> We’ll simulate transferring to someone else by creating a new private account we own and treating it as if it belonged to another user. +> We’ll 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 recipient’s 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. diff --git a/indexer/core/src/lib.rs b/indexer/core/src/lib.rs index fc86ff40..44f0dc19 100644 --- a/indexer/core/src/lib.rs +++ b/indexer/core/src/lib.rs @@ -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() diff --git a/integration_tests/src/config.rs b/integration_tests/src/config.rs index 008f7700..faff1e79 100644 --- a/integration_tests/src/config.rs +++ b/integration_tests/src/config.rs @@ -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() diff --git a/integration_tests/tests/account.rs b/integration_tests/tests/account.rs index 60c1aeaa..47fda69f 100644 --- a/integration_tests/tests/account.rs +++ b/integration_tests/tests/account.rs @@ -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(()) } diff --git a/integration_tests/tests/amm.rs b/integration_tests/tests/amm.rs index dde9e7f5..3eaf35e2 100644 --- a/integration_tests/tests/amm.rs +++ b/integration_tests/tests/amm.rs @@ -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?; diff --git a/integration_tests/tests/ata.rs b/integration_tests/tests/ata.rs index c0918635..6f0bf05c 100644 --- a/integration_tests/tests/ata.rs +++ b/integration_tests/tests/ata.rs @@ -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, }), ) diff --git a/integration_tests/tests/auth_transfer/private.rs b/integration_tests/tests/auth_transfer/private.rs index cf02d0ac..8db5f8d4 100644 --- a/integration_tests/tests/auth_transfer/private.rs +++ b/integration_tests/tests/auth_transfer/private.rs @@ -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(()) +} diff --git a/integration_tests/tests/auth_transfer/public.rs b/integration_tests/tests/auth_transfer/public.rs index 416c4490..e2b5a618 100644 --- a/integration_tests/tests/auth_transfer/public.rs +++ b/integration_tests/tests/auth_transfer/public.rs @@ -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, }); diff --git a/integration_tests/tests/indexer.rs b/integration_tests/tests/indexer.rs index f40b3607..21463117 100644 --- a/integration_tests/tests/indexer.rs +++ b/integration_tests/tests/indexer.rs @@ -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, }); diff --git a/integration_tests/tests/indexer_ffi.rs b/integration_tests/tests/indexer_ffi.rs index 5495e6c6..ac1dab4c 100644 --- a/integration_tests/tests/indexer_ffi.rs +++ b/integration_tests/tests/indexer_ffi.rs @@ -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))?; diff --git a/integration_tests/tests/keys_restoration.rs b/integration_tests/tests/keys_restoration.rs index 8dca027c..ff339120 100644 --- a/integration_tests/tests/keys_restoration.rs +++ b/integration_tests/tests/keys_restoration.rs @@ -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?; diff --git a/integration_tests/tests/token.rs b/integration_tests/tests/token.rs index e40e27c8..6db718f9 100644 --- a/integration_tests/tests/token.rs +++ b/integration_tests/tests/token.rs @@ -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?; diff --git a/integration_tests/tests/tps.rs b/integration_tests/tests/tps.rs index cd8e9ae9..f1167829 100644 --- a/integration_tests/tests/tps.rs +++ b/integration_tests/tests/tps.rs @@ -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); @@ -253,10 +256,12 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { ssk: sender_ss, nsk: sender_nsk, membership_proof: proof, + identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { npk: recipient_npk, ssk: recipient_ss, + identifier: 0, }, ], &program.into(), diff --git a/integration_tests/tests/wallet_ffi.rs b/integration_tests/tests/wallet_ffi.rs index ac548280..db84b066 100644 --- a/integration_tests/tests/wallet_ffi.rs +++ b/integration_tests/tests/wallet_ffi.rs @@ -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, ); diff --git a/key_protocol/src/key_management/key_tree/keys_private.rs b/key_protocol/src/key_management/key_tree/keys_private.rs index 42130b1f..6ffc8119 100644 --- a/key_protocol/src/key_management/key_tree/keys_private.rs +++ b/key_protocol/src/key_management/key_tree/keys_private.rs @@ -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, } -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 { - 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 { + self.value.1.iter().map(|(identifier, _)| { + nssa::AccountId::from((&self.value.0.nullifier_public_key, *identifier)) + }) } } diff --git a/key_protocol/src/key_management/key_tree/keys_public.rs b/key_protocol/src/key_management/key_tree/keys_public.rs index d4c32b4a..3ab9cc35 100644 --- a/key_protocol/src/key_management/key_tree/keys_public.rs +++ b/key_protocol/src/key_management/key_tree/keys_public.rs @@ -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 { - 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 { + std::iter::once(self.account_id()) + } +} + #[cfg(test)] mod tests { use nssa::{PrivateKey, PublicKey}; diff --git a/key_protocol/src/key_management/key_tree/mod.rs b/key_protocol/src/key_management/key_tree/mod.rs index 08a576e5..0ae0a52f 100644 --- a/key_protocol/src/key_management/key_tree/mod.rs +++ b/key_protocol/src/key_management/key_tree/mod.rs @@ -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 { +pub struct KeyTree { pub key_map: BTreeMap, pub account_id_map: BTreeMap, } @@ -28,7 +29,7 @@ pub struct KeyTree { pub type KeyTreePublic = KeyTree; pub type KeyTreePrivate = KeyTree; -impl KeyTree { +impl KeyTree { #[must_use] pub fn new(seed: &SeedHolder) -> Self { let seed_fit: [u8; 64] = seed @@ -37,29 +38,62 @@ impl KeyTree { .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 { + 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 { + 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 { @@ -102,25 +136,6 @@ impl KeyTree { } } - 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 KeyTree { } } - 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 { 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 { - 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 KeyTree { } } } -} -impl KeyTree { - /// 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 { + let chain_index = self.account_id_map.remove(&addr)?; + self.key_map.remove(&chain_index) } } impl KeyTree { + /// 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 { } } +impl KeyTree { + pub fn create_private_accounts_key_node( + &mut self, + parent_cci: &ChainIndex, + ) -> Option { + self.generate_new_node(parent_cci) + } + + pub fn create_private_accounts_key_node_layered(&mut self) -> Option { + 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 { + 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); } } diff --git a/key_protocol/src/key_management/key_tree/traits.rs b/key_protocol/src/key_management/key_tree/traits.rs index 65e8fae0..71ca4743 100644 --- a/key_protocol/src/key_management/key_tree/traits.rs +++ b/key_protocol/src/key_management/key_tree/traits.rs @@ -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; - - 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; } diff --git a/key_protocol/src/key_management/mod.rs b/key_protocol/src/key_management/mod.rs index c038c415..065af364 100644 --- a/key_protocol/src/key_management/mod.rs +++ b/key_protocol/src/key_management/mod.rs @@ -172,11 +172,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() diff --git a/key_protocol/src/key_protocol_core/mod.rs b/key_protocol/src/key_protocol_core/mod.rs index 8186865f..4df6df82 100644 --- a/key_protocol/src/key_protocol_core/mod.rs +++ b/key_protocol/src/key_protocol_core/mod.rs @@ -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::{ @@ -12,13 +14,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, /// Default private accounts. - pub default_user_private_accounts: - BTreeMap, + pub default_user_private_accounts: BTreeMap, /// Tree of public keys. pub public_key_tree: KeyTreePublic, /// Tree of private keys. @@ -42,13 +49,16 @@ impl NSSAUserData { } fn valid_private_key_transaction_pairing_check( - accounts_keys_map: &BTreeMap, + accounts_keys_map: &BTreeMap, ) -> 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; } } @@ -57,10 +67,7 @@ impl NSSAUserData { pub fn new_with_accounts( default_accounts_keys: BTreeMap, - default_accounts_key_chains: BTreeMap< - nssa::AccountId, - (KeyChain, nssa_core::account::Account), - >, + default_accounts_key_chains: BTreeMap, public_key_tree: KeyTreePublic, private_key_tree: KeyTreePrivate, ) -> Result { @@ -94,11 +101,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"), } } @@ -114,50 +121,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, - ) -> (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 { 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 { + 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 { @@ -200,16 +218,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:#?}"); } } diff --git a/nssa/core/src/account.rs b/nssa/core/src/account.rs index 0f9248e3..dc8a49a9 100644 --- a/nssa/core/src/account.rs +++ b/nssa/core/src/account.rs @@ -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); } diff --git a/nssa/core/src/circuit_io.rs b/nssa/core/src/circuit_io.rs index af78104d..e67c8538 100644 --- a/nssa/core/src/circuit_io.rs +++ b/nssa/core/src/circuit_io.rs @@ -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, @@ -29,11 +29,13 @@ pub enum InputAccountIdentity { /// commitment, ciphertext, or nullifier. Public, /// Init of an authorized standalone private account: no membership proof. The `pre_state` - /// must be `Account::default()`. `npk` is derived from `nsk` and matched against - /// `pre_state.account_id` via `AccountId::from(npk)`. + /// must be `Account::default()`. The account_id is derived as + /// `AccountId::from((&NullifierPublicKey::from(nsk), identifier))` and matched against + /// `pre_state.account_id`. PrivateAuthorizedInit { ssk: SharedSecretKey, nsk: NullifierSecretKey, + identifier: Identifier, }, /// Update of an authorized standalone private account: existing on-chain commitment, with /// membership proof. @@ -41,22 +43,25 @@ pub enum InputAccountIdentity { ssk: SharedSecretKey, nsk: NullifierSecretKey, membership_proof: MembershipProof, + identifier: Identifier, }, /// Init of a standalone private account the caller does not own (e.g. a recipient who /// doesn't yet exist on chain). No `nsk`, no membership proof. PrivateUnauthorized { npk: NullifierPublicKey, ssk: SharedSecretKey, + identifier: Identifier, }, /// Init of a private PDA, unauthorized. The npk-to-account_id binding is proven upstream - /// via `Claim::Pda(seed)` or a caller's `pda_seeds` match. + /// via `Claim::Pda(seed)` or a caller's `pda_seeds` match. Identifier is fixed by + /// convention to `PRIVATE_PDA_FIXED_IDENTIFIER` and not carried per-input. PrivatePdaInit { npk: NullifierPublicKey, ssk: SharedSecretKey, }, /// Update of an existing private PDA, authorized, with membership proof. `npk` is derived /// from `nsk`. Authorization is established upstream by a caller `pda_seeds` match or a - /// previously-seen authorization in a chained call. + /// previously-seen authorization in a chained call. Identifier is fixed. PrivatePdaUpdate { ssk: SharedSecretKey, nsk: NullifierSecretKey, @@ -121,7 +126,7 @@ mod tests { use super::*; use crate::{ - Commitment, Nullifier, NullifierPublicKey, + Commitment, Nullifier, account::{Account, AccountId, AccountWithMetadata, Nonce}, }; @@ -158,12 +163,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], diff --git a/nssa/core/src/commitment.rs b/nssa/core/src/commitment.rs index 24d5de87..73ccd703 100644 --- a/nssa/core/src/commitment.rs +++ b/nssa/core/src/commitment.rs @@ -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); } diff --git a/nssa/core/src/encryption/mod.rs b/nssa/core/src/encryption/mod.rs index 400fb331..80d62f30 100644 --- a/nssa/core/src/encryption/mod.rs +++ b/nssa/core/src/encryption/mod.rs @@ -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 { + ) -> 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)) } } diff --git a/nssa/core/src/lib.rs b/nssa/core/src/lib.rs index f1e2e4f5..d660aed0 100644 --- a/nssa/core/src/lib.rs +++ b/nssa/core/src/lib.rs @@ -11,7 +11,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; diff --git a/nssa/core/src/nullifier.rs b/nssa/core/src/nullifier.rs index fd17b391..aafe3f7c 100644 --- a/nssa/core/src/nullifier.rs +++ b/nssa/core/src/nullifier.rs @@ -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); } diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 5091cdff..1ef2ef6c 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -913,18 +913,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. diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index 0b907ab6..8cfbab84 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -2,8 +2,7 @@ use std::collections::{HashMap, VecDeque}; use borsh::{BorshDeserialize, BorshSerialize}; use nssa_core::{ - PrivacyPreservingCircuitInput, InputAccountIdentity, - PrivacyPreservingCircuitOutput, + InputAccountIdentity, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, account::AccountWithMetadata, program::{ChainedCall, InstructionData, ProgramId, ProgramOutput}, }; @@ -206,11 +205,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; @@ -224,7 +220,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(), }; @@ -241,6 +237,7 @@ mod tests { InputAccountIdentity::PrivateUnauthorized { npk: recipient_keys.npk(), ssk: shared_secret, + identifier: 0, }, ], &Program::authenticated_transfer_program().into(), @@ -257,7 +254,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], @@ -282,27 +279,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, ), ]; @@ -318,12 +312,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,10 +336,12 @@ mod tests { membership_proof: commitment_set .get_proof_for(&commitment_sender) .expect("sender's commitment must be in the set"), + identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { npk: recipient_keys.npk(), ssk: shared_secret_2, + identifier: 0, }, ], &program.into(), @@ -359,7 +355,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], @@ -368,7 +364,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], @@ -384,7 +380,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(); @@ -413,6 +409,7 @@ mod tests { vec![InputAccountIdentity::PrivateUnauthorized { npk: account_keys.npk(), ssk: shared_secret, + identifier: 0, }], &program_with_deps, ); diff --git a/nssa/src/privacy_preserving_transaction/message.rs b/nssa/src/privacy_preserving_transaction/message.rs index 85f4a202..ee46d0b3 100644 --- a/nssa/src/privacy_preserving_transaction/message.rs +++ b/nssa/src/privacy_preserving_transaction/message.rs @@ -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()); diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 724ea4aa..5eb4ea39 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -362,8 +362,8 @@ pub mod tests { use std::collections::HashMap; use nssa_core::{ - BlockId, Commitment, Nullifier, NullifierPublicKey, NullifierSecretKey, - InputAccountIdentity, SharedSecretKey, Timestamp, + BlockId, Commitment, InputAccountIdentity, Nullifier, NullifierPublicKey, + NullifierSecretKey, SharedSecretKey, Timestamp, account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, encryption::{EphemeralPublicKey, Scalar, ViewingPublicKey}, program::{ @@ -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()); @@ -1297,6 +1299,7 @@ pub mod tests { InputAccountIdentity::PrivateUnauthorized { npk: recipient_keys.npk(), ssk: shared_secret, + identifier: 0, }, ], &Program::authenticated_transfer_program().into(), @@ -1323,11 +1326,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()); @@ -1347,10 +1354,12 @@ pub mod tests { membership_proof: state .get_proof_for_commitment(&sender_commitment) .expect("sender's commitment must be in state"), + identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { npk: recipient_keys.npk(), ssk: shared_secret_2, + identifier: 0, }, ], &program.into(), @@ -1381,9 +1390,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, @@ -1404,6 +1417,7 @@ pub mod tests { membership_proof: state .get_proof_for_commitment(&sender_commitment) .expect("sender's commitment must be in state"), + identifier: 0, }, InputAccountIdentity::Public, ], @@ -1491,8 +1505,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), @@ -1501,15 +1517,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() }, @@ -1568,8 +1584,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), @@ -1578,7 +1595,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); @@ -1772,10 +1789,7 @@ pub mod tests { let result = execute_and_prove( vec![public_account_1, public_account_2], Program::serialize_instruction(()).unwrap(), - vec![ - InputAccountIdentity::Public, - InputAccountIdentity::Public, - ], + vec![InputAccountIdentity::Public, InputAccountIdentity::Public], &program.into(), ); @@ -1830,10 +1844,7 @@ pub mod tests { let result = execute_and_prove( vec![public_account_1, public_account_2], Program::serialize_instruction(10_u128).unwrap(), - vec![ - InputAccountIdentity::Public, - InputAccountIdentity::Public, - ], + vec![InputAccountIdentity::Public, InputAccountIdentity::Public], &program.into(), ); @@ -1862,7 +1873,7 @@ pub mod tests { AccountId::new([1; 32]), ); - // Single account entry for a circuit execution with two pre_state accounts. + // Single account_identity entry for a circuit execution with two pre_state accounts. let result = execute_and_prove( vec![public_account_1, public_account_2], Program::serialize_instruction(10_u128).unwrap(), @@ -1885,10 +1896,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 the recipient nsk to authorize the sender. // This should be set to the sender private account in a normal circumstance. @@ -1902,10 +1913,12 @@ pub mod tests { ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), nsk: recipient_keys.nsk, membership_proof: (0, vec![]), + identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { npk: recipient_keys.npk(), ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + identifier: 0, }, ], &program.into(), @@ -1926,7 +1939,7 @@ pub mod tests { ..Account::default() }, true, - &sender_keys.npk(), + (&sender_keys.npk(), 0), ); let private_account_2 = AccountWithMetadata::new( Account { @@ -1935,7 +1948,7 @@ pub mod tests { ..Account::default() }, false, - &recipient_keys.npk(), + (&recipient_keys.npk(), 0), ); let result = execute_and_prove( @@ -1946,10 +1959,12 @@ pub mod tests { ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), nsk: sender_keys.nsk, membership_proof: (0, vec![]), + identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { npk: recipient_keys.npk(), ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + identifier: 0, }, ], &program.into(), @@ -1970,7 +1985,7 @@ pub mod tests { ..Account::default() }, true, - &sender_keys.npk(), + (&sender_keys.npk(), 0), ); let private_account_2 = AccountWithMetadata::new( Account { @@ -1979,7 +1994,7 @@ pub mod tests { ..Account::default() }, false, - &recipient_keys.npk(), + (&recipient_keys.npk(), 0), ); let result = execute_and_prove( @@ -1990,10 +2005,12 @@ pub mod tests { ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), nsk: sender_keys.nsk, membership_proof: (0, vec![]), + identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { npk: recipient_keys.npk(), ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + identifier: 0, }, ], &program.into(), @@ -2014,7 +2031,7 @@ pub mod tests { ..Account::default() }, true, - &sender_keys.npk(), + (&sender_keys.npk(), 0), ); let private_account_2 = AccountWithMetadata::new( Account { @@ -2023,7 +2040,7 @@ pub mod tests { ..Account::default() }, false, - &recipient_keys.npk(), + (&recipient_keys.npk(), 0), ); let result = execute_and_prove( @@ -2034,10 +2051,12 @@ pub mod tests { ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), nsk: sender_keys.nsk, membership_proof: (0, vec![]), + identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { npk: recipient_keys.npk(), ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + identifier: 0, }, ], &program.into(), @@ -2058,7 +2077,7 @@ pub mod tests { ..Account::default() }, true, - &sender_keys.npk(), + (&sender_keys.npk(), 0), ); let private_account_2 = AccountWithMetadata::new( Account { @@ -2067,7 +2086,7 @@ pub mod tests { ..Account::default() }, false, - &recipient_keys.npk(), + (&recipient_keys.npk(), 0), ); let result = execute_and_prove( @@ -2078,10 +2097,12 @@ pub mod tests { ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), nsk: sender_keys.nsk, membership_proof: (0, vec![]), + identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { npk: recipient_keys.npk(), ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + identifier: 0, }, ], &program.into(), @@ -2103,13 +2124,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( @@ -2120,10 +2141,12 @@ pub mod tests { ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), nsk: sender_keys.nsk, membership_proof: (0, vec![]), + identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { npk: recipient_keys.npk(), ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + identifier: 0, }, ], &program.into(), @@ -2154,16 +2177,14 @@ pub mod tests { let private_pda_account = AccountWithMetadata::new(Account::default(), false, AccountId::new([1; 32])); + let visibility_mask = [0, 3]; let result = execute_and_prove( vec![public_account_1, private_pda_account], Program::serialize_instruction(10_u128).unwrap(), - vec![ - InputAccountIdentity::Public, - InputAccountIdentity::PrivatePdaInit { - npk, - ssk: shared_secret, - }, - ], + visibility_mask.to_vec(), + vec![(npk, 0, shared_secret)], + vec![], + vec![None], &program.into(), ); @@ -2461,7 +2482,7 @@ pub mod tests { ..Account::default() }, true, - &sender_keys.npk(), + (&sender_keys.npk(), 0), ); let shared_secret = SharedSecretKey::new(&[55; 32], &sender_keys.vpk()); @@ -2473,11 +2494,13 @@ pub mod tests { ssk: shared_secret, nsk: sender_keys.nsk, membership_proof: (1, vec![]), + identifier: 0, }, InputAccountIdentity::PrivateAuthorizedUpdate { ssk: shared_secret, nsk: sender_keys.nsk, membership_proof: (1, vec![]), + identifier: 0, }, ], &program.into(), @@ -2787,14 +2810,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)); @@ -2814,6 +2839,7 @@ pub mod tests { membership_proof: state .get_proof_for_commitment(&sender_commitment) .expect("sender's commitment must be in state"), + identifier: 0, }, InputAccountIdentity::Public, ], @@ -2866,7 +2892,7 @@ pub mod tests { ..Account::default() }, true, - &from_keys.npk(), + (&from_keys.npk(), 0), ); let to_account = AccountWithMetadata::new( Account { @@ -2874,13 +2900,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![ @@ -2919,14 +2947,14 @@ 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( @@ -2939,6 +2967,7 @@ pub mod tests { membership_proof: state .get_proof_for_commitment(&from_commitment) .expect("from's commitment must be in state"), + identifier: 0, }, InputAccountIdentity::PrivateAuthorizedUpdate { ssk: from_ss, @@ -2946,6 +2975,7 @@ pub mod tests { membership_proof: state .get_proof_for_commitment(&to_commitment) .expect("to's commitment must be in state"), + identifier: 0, }, ], &program_with_deps, @@ -3194,7 +3224,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(); @@ -3210,12 +3240,11 @@ pub mod tests { let (output, proof) = execute_and_prove( vec![authorized_account], Program::serialize_instruction(balance).unwrap(), - vec![ - InputAccountIdentity::PrivateAuthorizedInit { - ssk: shared_secret, - nsk: private_keys.nsk, - }, - ], + vec![InputAccountIdentity::PrivateAuthorizedInit { + ssk: shared_secret, + nsk: private_keys.nsk, + identifier: 0, + }], &program.into(), ) .unwrap(); @@ -3235,7 +3264,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)); } @@ -3249,7 +3279,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]; @@ -3262,6 +3292,7 @@ pub mod tests { vec![InputAccountIdentity::PrivateUnauthorized { npk: private_keys.npk(), ssk: shared_secret, + identifier: 0, }], &program.into(), ) @@ -3282,7 +3313,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)); } @@ -3295,7 +3327,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(); @@ -3310,12 +3342,11 @@ pub mod tests { let (output, proof) = execute_and_prove( vec![authorized_account.clone()], Program::serialize_instruction(balance).unwrap(), - vec![ - InputAccountIdentity::PrivateAuthorizedInit { - ssk: shared_secret, - nsk: private_keys.nsk, - }, - ], + vec![InputAccountIdentity::PrivateAuthorizedInit { + ssk: shared_secret, + nsk: private_keys.nsk, + identifier: 0, + }], &claimer_program.into(), ) .unwrap(); @@ -3339,7 +3370,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 @@ -3357,12 +3389,11 @@ pub mod tests { let res = execute_and_prove( vec![account_metadata], Program::serialize_instruction(()).unwrap(), - vec![ - InputAccountIdentity::PrivateAuthorizedInit { - ssk: shared_secret2, - nsk: private_keys.nsk, - }, - ], + vec![InputAccountIdentity::PrivateAuthorizedInit { + ssk: shared_secret2, + nsk: private_keys.nsk, + identifier: 0, + }], &noop_program.into(), ); @@ -3428,20 +3459,19 @@ 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>, bool) = (None, false); let result = execute_and_prove( vec![private_account], Program::serialize_instruction(instruction).unwrap(), - vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new(&[3; 32], &sender_keys.vpk()), - nsk: sender_keys.nsk, - membership_proof: (0, vec![]), - }, - ], + vec![InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: SharedSecretKey::new(&[3; 32], &sender_keys.vpk()), + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + identifier: 0, + }], &program.into(), ); @@ -3454,7 +3484,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>, bool) = (Some(new_data), false); @@ -3462,13 +3492,12 @@ pub mod tests { let result = execute_and_prove( vec![private_account], Program::serialize_instruction(instruction).unwrap(), - vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new(&[3; 32], &sender_keys.vpk()), - nsk: sender_keys.nsk, - membership_proof: (0, vec![]), - }, - ], + vec![InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: SharedSecretKey::new(&[3; 32], &sender_keys.vpk()), + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + identifier: 0, + }], &program.into(), ); @@ -3494,11 +3523,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)], @@ -3528,6 +3558,7 @@ pub mod tests { membership_proof: state .get_proof_for_commitment(&recipient_commitment) .expect("recipient's commitment must be in state"), + identifier: 0, }, ], &program_with_deps, @@ -3662,7 +3693,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]; @@ -3679,6 +3710,7 @@ pub mod tests { vec![InputAccountIdentity::PrivateUnauthorized { npk: account_keys.npk(), ssk: shared_secret, + identifier: 0, }], &validity_window_program.into(), ) @@ -3731,7 +3763,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]; @@ -3748,6 +3780,7 @@ pub mod tests { vec![InputAccountIdentity::PrivateUnauthorized { npk: account_keys.npk(), ssk: shared_secret, + identifier: 0, }], &validity_window_program.into(), ) diff --git a/program_methods/guest/src/bin/privacy_preserving_circuit.rs b/program_methods/guest/src/bin/privacy_preserving_circuit.rs index 7aef273a..8930c57e 100644 --- a/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -4,9 +4,9 @@ use std::{ }; use nssa_core::{ - Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, MembershipProof, - Nullifier, NullifierPublicKey, NullifierSecretKey, PrivacyPreservingCircuitInput, - InputAccountIdentity, PrivacyPreservingCircuitOutput, SharedSecretKey, + Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, Identifier, + InputAccountIdentity, 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: Identifier = u128::MAX; + /// State of the involved accounts before and after program execution. struct ExecutionState { pre_states: Vec, @@ -504,12 +506,20 @@ fn compute_circuit_output( output.public_pre_states.push(pre_state); output.public_post_states.push(post_state); } - InputAccountIdentity::PrivateAuthorizedInit { ssk, nsk } => { + InputAccountIdentity::PrivateAuthorizedInit { + ssk, + nsk, + identifier, + } => { + assert_ne!( + *identifier, PRIVATE_PDA_FIXED_IDENTIFIER, + "Identifier must be different from {PRIVATE_PDA_FIXED_IDENTIFIER}. This is reserved for private PDA." + ); let npk = NullifierPublicKey::from(nsk); + let account_id = AccountId::from((&npk, *identifier)); assert_eq!( - AccountId::from(&npk), - pre_state.account_id, + account_id, pre_state.account_id, "AccountId mismatch" ); assert!( @@ -523,7 +533,7 @@ fn compute_circuit_output( ); let new_nullifier = ( - Nullifier::for_account_initialization(&npk), + Nullifier::for_account_initialization(&account_id), DUMMY_COMMITMENT_HASH, ); let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk); @@ -532,7 +542,8 @@ fn compute_circuit_output( &mut output, &mut output_index, post_state, - &npk, + &account_id, + *identifier, ssk, new_nullifier, new_nonce, @@ -542,12 +553,17 @@ fn compute_circuit_output( ssk, nsk, membership_proof, + identifier, } => { + assert_ne!( + *identifier, PRIVATE_PDA_FIXED_IDENTIFIER, + "Identifier must be different from {PRIVATE_PDA_FIXED_IDENTIFIER}. This is reserved for private PDA." + ); let npk = NullifierPublicKey::from(nsk); + let account_id = AccountId::from((&npk, *identifier)); assert_eq!( - AccountId::from(&npk), - pre_state.account_id, + account_id, pre_state.account_id, "AccountId mismatch" ); assert!( @@ -558,7 +574,7 @@ fn compute_circuit_output( let new_nullifier = compute_update_nullifier_and_set_digest( membership_proof, &pre_state.account, - &npk, + &account_id, nsk, ); let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk); @@ -567,16 +583,26 @@ fn compute_circuit_output( &mut output, &mut output_index, post_state, - &npk, + &account_id, + *identifier, ssk, new_nullifier, new_nonce, ); } - InputAccountIdentity::PrivateUnauthorized { npk, ssk } => { + InputAccountIdentity::PrivateUnauthorized { + npk, + ssk, + identifier, + } => { + 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!( - AccountId::from(npk), - pre_state.account_id, + account_id, pre_state.account_id, "AccountId mismatch" ); assert_eq!( @@ -590,25 +616,29 @@ fn compute_circuit_output( ); let new_nullifier = ( - Nullifier::for_account_initialization(npk), + Nullifier::for_account_initialization(&account_id), DUMMY_COMMITMENT_HASH, ); - let new_nonce = Nonce::private_account_nonce_init(npk); + let new_nonce = Nonce::private_account_nonce_init(&account_id); emit_private_output( &mut output, &mut output_index, post_state, - npk, + &account_id, + *identifier, ssk, new_nullifier, new_nonce, ); } - InputAccountIdentity::PrivatePdaInit { npk, ssk } => { + InputAccountIdentity::PrivatePdaInit { npk: _, ssk } => { // The npk-to-account_id binding is established upstream in // `validate_and_sync_states` via `Claim::Pda(seed)` or a caller `pda_seeds` - // match. Here we only enforce the init pre-conditions. + // match. Here we only enforce the init pre-conditions. The supplied npk on + // the variant has been recorded into `private_pda_npk_by_position` and used + // for the binding check; we use `pre_state.account_id` directly for nullifier + // and commitment derivation. assert!( !pre_state.is_authorized, "PrivatePdaInit requires unauthorized pre_state" @@ -620,16 +650,18 @@ fn compute_circuit_output( ); let new_nullifier = ( - Nullifier::for_account_initialization(npk), + Nullifier::for_account_initialization(&pre_state.account_id), DUMMY_COMMITMENT_HASH, ); - let new_nonce = Nonce::private_account_nonce_init(npk); + let new_nonce = Nonce::private_account_nonce_init(&pre_state.account_id); + let account_id = pre_state.account_id; emit_private_output( &mut output, &mut output_index, post_state, - npk, + &account_id, + PRIVATE_PDA_FIXED_IDENTIFIER, ssk, new_nullifier, new_nonce, @@ -640,8 +672,6 @@ fn compute_circuit_output( nsk, membership_proof, } => { - let npk = NullifierPublicKey::from(nsk); - // The npk binding is established upstream. Authorization must already be set; // an unauthorized PrivatePdaUpdate would mean the prover supplied an nsk for an // unbound PDA, which the upstream binding check would have rejected anyway, @@ -654,16 +684,18 @@ fn compute_circuit_output( let new_nullifier = compute_update_nullifier_and_set_digest( membership_proof, &pre_state.account, - &npk, + &pre_state.account_id, nsk, ); let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk); + let account_id = pre_state.account_id; emit_private_output( &mut output, &mut output_index, post_state, - &npk, + &account_id, + PRIVATE_PDA_FIXED_IDENTIFIER, ssk, new_nullifier, new_nonce, @@ -675,11 +707,16 @@ fn compute_circuit_output( output } +#[expect( + clippy::too_many_arguments, + reason = "All seven inputs are distinct concerns from the variant arms; bundling would be artificial" +)] fn emit_private_output( output: &mut PrivacyPreservingCircuitOutput, output_index: &mut u32, post_state: Account, - npk: &NullifierPublicKey, + account_id: &AccountId, + identifier: Identifier, shared_secret: &SharedSecretKey, new_nullifier: (Nullifier, CommitmentSetDigest), new_nonce: Nonce, @@ -689,9 +726,10 @@ fn emit_private_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(account_id, &post_with_updated_nonce); let encrypted_account = EncryptionScheme::encrypt( &post_with_updated_nonce, + identifier, shared_secret, &commitment_post, *output_index, @@ -707,10 +745,10 @@ fn emit_private_output( fn compute_update_nullifier_and_set_digest( membership_proof: &MembershipProof, pre_account: &Account, - npk: &NullifierPublicKey, + account_id: &AccountId, nsk: &NullifierSecretKey, ) -> (Nullifier, CommitmentSetDigest) { - 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); let nullifier = Nullifier::for_account_update(&commitment_pre, nsk); (nullifier, set_digest) diff --git a/sequencer/core/src/lib.rs b/sequencer/core/src/lib.rs index 0c5556b3..5f118a2f 100644 --- a/sequencer/core/src/lib.rs +++ b/sequencer/core/src/lib.rs @@ -110,6 +110,7 @@ impl SequencerCore SequencerCore 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 Vec Vec 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 ); diff --git a/wallet-ffi/src/account.rs b/wallet-ffi/src/account.rs index 49f6a8de..6214ab01 100644 --- a/wallet-ffi/src/account.rs +++ b/wallet-ffi/src/account.rs @@ -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. diff --git a/wallet-ffi/src/keys.rs b/wallet-ffi/src/keys.rs index 4eeadd8f..0471f255 100644 --- a/wallet-ffi/src/keys.rs +++ b/wallet-ffi/src/keys.rs @@ -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; diff --git a/wallet-ffi/src/transfer.rs b/wallet-ffi/src/transfer.rs index 739832ae..f2cadacc 100644 --- a/wallet-ffi/src/transfer.rs +++ b/wallet-ffi/src/transfer.rs @@ -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); diff --git a/wallet-ffi/wallet_ffi.h b/wallet-ffi/wallet_ffi.h index 2665cd40..89026950 100644 --- a/wallet-ffi/wallet_ffi.h +++ b/wallet-ffi/wallet_ffi.h @@ -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); diff --git a/wallet/configs/debug/wallet_config.json b/wallet/configs/debug/wallet_config.json index 6604f65b..94e13ebd 100644 --- a/wallet/configs/debug/wallet_config.json +++ b/wallet/configs/debug/wallet_config.json @@ -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, diff --git a/wallet/src/chain_storage.rs b/wallet/src/chain_storage.rs index 3699609b..8d168d8e 100644 --- a/wallet/src/chain_storage.rs +++ b/wallet/src/chain_storage.rs @@ -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, })), diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index 86ae7e35..b5e80854 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -82,7 +82,8 @@ pub enum NewSubcommand { /// Label to assign to the new account. label: Option, }, - /// 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, }, + /// 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, + }, } 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 } => { diff --git a/wallet/src/cli/programs/native_token_transfer.rs b/wallet/src/cli/programs/native_token_transfer.rs index 41008eac..f02ff99c 100644 --- a/wallet/src/cli/programs/native_token_transfer.rs +++ b/wallet/src/cli/programs/native_token_transfer.rs @@ -59,6 +59,10 @@ pub enum AuthTransferSubcommand { /// `to_vpk` - valid 33 byte hex string. #[arg(long)] to_vpk: Option, + /// 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, /// 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, /// 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, /// 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}"); diff --git a/wallet/src/cli/programs/token.rs b/wallet/src/cli/programs/token.rs index 0575da09..73bb6c2c 100644 --- a/wallet/src/cli/programs/token.rs +++ b/wallet/src/cli/programs/token.rs @@ -73,6 +73,10 @@ pub enum TokenProgramAgnosticSubcommand { /// `to_vpk` - valid 33 byte hex string. #[arg(long)] to_vpk: Option, + /// 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, /// 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, + /// 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, /// 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, #[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, #[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, #[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, #[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?; diff --git a/wallet/src/config.rs b/wallet/src/config.rs index 33527009..bbd98ac7 100644 --- a/wallet/src/config.rs +++ b/wallet/src/config.rs @@ -28,7 +28,7 @@ pub struct PersistentAccountDataPublic { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PersistentAccountDataPrivate { - pub account_id: nssa::AccountId, + pub identifiers: Vec, 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 for InitialAccountData { fn from(value: PublicAccountPrivateInitialData) -> Self { Self::Public(value) diff --git a/wallet/src/helperfunctions.rs b/wallet/src/helperfunctions.rs index 37a27409..94755f6e 100644 --- a/wallet/src/helperfunctions.rs +++ b/wallet/src/helperfunctions.rs @@ -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 { diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index e35c7c46..c8244ef9 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -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 { + self.storage + .user_data + .create_private_accounts_key(chain_index) + } + pub fn create_new_account_private( &mut self, chain_index: Option, ) -> (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 { - 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 => {} } @@ -477,33 +500,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); @@ -515,18 +538,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::>() }) .collect::>(); - 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); } } diff --git a/wallet/src/privacy_preserving_tx.rs b/wallet/src/privacy_preserving_tx.rs index 839e2da3..a4ed970f 100644 --- a/wallet/src/privacy_preserving_tx.rs +++ b/wallet/src/privacy_preserving_tx.rs @@ -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, InputAccountIdentity, + Identifier, InputAccountIdentity, MembershipProof, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, account::{AccountWithMetadata, Nonce}, encryption::{EphemeralPublicKey, ViewingPublicKey}, @@ -17,6 +17,7 @@ pub enum PrivacyPreservingAccount { PrivateForeign { npk: NullifierPublicKey, vpk: ViewingPublicKey, + identifier: Identifier, }, } @@ -30,7 +31,12 @@ impl PrivacyPreservingAccount { pub const fn is_private(&self) -> bool { matches!( &self, - Self::PrivateOwned(_) | Self::PrivateForeign { npk: _, vpk: _ } + Self::PrivateOwned(_) + | Self::PrivateForeign { + npk: _, + vpk: _, + identifier: _, + } ) } } @@ -79,15 +85,20 @@ impl AccountManager { State::Private(pre) } - 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 eph_holder = EphemeralKeyHolder::new(&npk); let ssk = eph_holder.calculate_shared_secret_sender(&vpk); let epk = eph_holder.generate_ephemeral_public_key(); let pre = AccountPreparedData { nsk: None, npk, + identifier, vpk, pre_state: auth_acc, proof: None, @@ -155,17 +166,18 @@ impl AccountManager { ssk: pre.ssk, nsk, membership_proof, + identifier: pre.identifier, } } - (Some(nsk), None) => { - InputAccountIdentity::PrivateAuthorizedInit { - ssk: pre.ssk, - nsk, - } - } + (Some(nsk), None) => InputAccountIdentity::PrivateAuthorizedInit { + ssk: pre.ssk, + nsk, + identifier: pre.identifier, + }, (None, _) => InputAccountIdentity::PrivateUnauthorized { npk: pre.npk, ssk: pre.ssk, + identifier: pre.identifier, }, }, }) @@ -196,11 +208,12 @@ impl AccountManager { struct AccountPreparedData { nsk: Option, npk: NullifierPublicKey, + identifier: Identifier, vpk: ViewingPublicKey, pre_state: AccountWithMetadata, proof: Option, /// Cached shared-secret key derived once at `AccountManager::new`. Reused for both the - /// circuit input variant (`accounts()`) and the message ephemeral-key tuples + /// circuit input variant (`account_identities()`) and the message ephemeral-key tuples /// (`private_account_keys()`), so all consumers see the same key. The corresponding /// `EphemeralKeyHolder` uses `OsRng` and would produce a different value on a second call. ssk: SharedSecretKey, @@ -212,11 +225,8 @@ async fn private_acc_preparation( wallet: &WalletCore, account_id: AccountId, ) -> Result { - 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); }; @@ -234,7 +244,7 @@ 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)); let eph_holder = EphemeralKeyHolder::new(&from_npk); let ssk = eph_holder.calculate_shared_secret_sender(&from_vpk); @@ -243,6 +253,7 @@ async fn private_acc_preparation( Ok(AccountPreparedData { nsk: Some(nsk), npk: from_npk, + identifier: from_identifier, vpk: from_vpk, pre_state: sender_pre, proof, diff --git a/wallet/src/program_facades/native_token_transfer/private.rs b/wallet/src/program_facades/native_token_transfer/private.rs index c3a2125b..d317b31c 100644 --- a/wallet/src/program_facades/native_token_transfer/private.rs +++ b/wallet/src/program_facades/native_token_transfer/private.rs @@ -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, diff --git a/wallet/src/program_facades/native_token_transfer/shielded.rs b/wallet/src/program_facades/native_token_transfer/shielded.rs index 625e1a8b..8f7ba2b5 100644 --- a/wallet/src/program_facades/native_token_transfer/shielded.rs +++ b/wallet/src/program_facades/native_token_transfer/shielded.rs @@ -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, diff --git a/wallet/src/program_facades/token.rs b/wallet/src/program_facades/token.rs index 1f941c8c..d105a4de 100644 --- a/wallet/src/program_facades/token.rs +++ b/wallet/src/program_facades/token.rs @@ -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,