diff --git a/Cargo.lock b/Cargo.lock index 5a34b7e9..73fbb12a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1111,7 +1111,7 @@ dependencies = [ "log", "num", "pin-project-lite", - "rand 0.9.2", + "rand 0.9.3", "rustls", "rustls-native-certs", "rustls-pki-types", @@ -2506,7 +2506,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb330bbd4cb7a5b9f559427f06f98a4f853a137c8298f3bd3f8ca57663e21986" dependencies = [ "portable-atomic", - "rand 0.9.2", + "rand 0.9.3", "web-time", ] @@ -3478,6 +3478,16 @@ dependencies = [ "url", ] +[[package]] +name = "indexer_ffi" +version = "0.1.0" +dependencies = [ + "cbindgen", + "indexer_service", + "log", + "tokio", +] + [[package]] name = "indexer_service" version = "0.1.0" @@ -3598,6 +3608,7 @@ dependencies = [ "env_logger", "futures", "hex", + "indexer_ffi", "indexer_service", "indexer_service_rpc", "key_protocol", @@ -3847,7 +3858,7 @@ dependencies = [ "jsonrpsee-types", "parking_lot", "pin-project", - "rand 0.9.2", + "rand 0.9.3", "rustc-hash", "serde", "serde_json", @@ -4073,7 +4084,7 @@ dependencies = [ "oco_ref", "or_poisoned", "paste", - "rand 0.9.2", + "rand 0.9.3", "reactive_graph", "rustc-hash", "rustc_version", @@ -5744,7 +5755,7 @@ dependencies = [ "futures-util", "opentelemetry", "percent-encoding", - "rand 0.9.2", + "rand 0.9.3", "thiserror 2.0.18", "tokio", "tokio-stream", @@ -6128,7 +6139,7 @@ checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" dependencies = [ "bitflags 2.11.0", "num-traits", - "rand 0.9.2", + "rand 0.9.3", "rand_chacha 0.9.0", "rand_xorshift", "unarray", @@ -6304,7 +6315,7 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.2", + "rand 0.9.3", "ring", "rustc-hash", "rustls", @@ -6392,9 +6403,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -6696,7 +6707,7 @@ dependencies = [ "elf", "lazy_static", "postcard", - "rand 0.9.2", + "rand 0.9.3", "risc0-zkp", "risc0-zkvm-platform", "ruint", @@ -6792,7 +6803,7 @@ dependencies = [ "hex", "lazy-regex", "metal", - "rand 0.9.2", + "rand 0.9.3", "rayon", "risc0-circuit-recursion-sys", "risc0-core", @@ -6836,7 +6847,7 @@ dependencies = [ "num-traits", "paste", "postcard", - "rand 0.9.2", + "rand 0.9.3", "rayon", "ringbuffer", "risc0-binfmt", @@ -6943,7 +6954,7 @@ dependencies = [ "ndarray", "parking_lot", "paste", - "rand 0.9.2", + "rand 0.9.3", "rand_core 0.9.5", "rayon", "risc0-core", @@ -6981,7 +6992,7 @@ dependencies = [ "num-traits", "object", "prost 0.13.5", - "rand 0.9.2", + "rand 0.9.3", "rayon", "risc0-binfmt", "risc0-build", @@ -7080,7 +7091,7 @@ dependencies = [ "futures", "light-poseidon", "quote", - "rand 0.9.2", + "rand 0.9.3", "syn 1.0.109", "thiserror 2.0.18", "tiny-keccak", @@ -7141,7 +7152,7 @@ dependencies = [ "borsh", "proptest", "rand 0.8.5", - "rand 0.9.2", + "rand 0.9.3", "ruint-macro", "serde_core", "valuable", @@ -8839,7 +8850,7 @@ dependencies = [ "http", "httparse", "log", - "rand 0.9.2", + "rand 0.9.3", "sha1", "thiserror 2.0.18", "utf-8", diff --git a/artifacts/program_methods/amm.bin b/artifacts/program_methods/amm.bin index 148a9403..36caad85 100644 Binary files a/artifacts/program_methods/amm.bin and b/artifacts/program_methods/amm.bin differ diff --git a/artifacts/program_methods/authenticated_transfer.bin b/artifacts/program_methods/authenticated_transfer.bin index ad40805f..bdbcef61 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 e2a6f120..d3ca0dab 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 d0460713..5e6a011b 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 b0f81f79..57a201c4 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 dcbee51a..dd613143 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 e0358fa4..6366eba6 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 9bd40a30..f9e4d1d4 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 0353d78f..94a90236 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 cd74cf3f..58331d6c 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 1f966bef..2760b7a3 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 8a48effd..ff504da1 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 e08df712..37c9a004 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 37abf0f7..3d69b8cb 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 ebd53621..873ce66a 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 29c660cd..0846f255 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 a560d477..1e285245 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 c9d0facd..cc757683 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 9b31fd7e..f152051d 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 c4a2c039..6d83b95b 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 42d2171d..29bcd715 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 d2b99291..c7cc1571 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 f57ac2f1..8f2b1e39 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 6b79e074..993c1451 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 eb89f4a9..579db977 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 092a2191..1a541384 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 559adea4..2b0d979a 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 d7e81a9f..4b55e871 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 880e03b1..3bdabade 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 3a4e811f..0aaf1a23 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 eeb80385..5700322e 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 b71d87ab..600b819d 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 8d749f3c..02ccc149 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 109829d2..d239c750 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/common/src/test_utils.rs b/common/src/test_utils.rs index 720bd2f9..267d10ce 100644 --- a/common/src/test_utils.rs +++ b/common/src/test_utils.rs @@ -3,7 +3,7 @@ use nssa::AccountId; use crate::{ HashType, block::{Block, HashableBlockData}, - transaction::NSSATransaction, + transaction::{NSSATransaction, clock_invocation}, }; // Helpers @@ -15,7 +15,7 @@ pub fn sequencer_sign_key_for_testing() -> nssa::PrivateKey { // Dummy producers -/// Produce dummy block with. +/// Produce dummy block with provided transactions + clock transaction an the end. /// /// `id` - block id, provide zero for genesis. /// @@ -26,8 +26,12 @@ pub fn sequencer_sign_key_for_testing() -> nssa::PrivateKey { pub fn produce_dummy_block( id: u64, prev_hash: Option, - transactions: Vec, + mut transactions: Vec, ) -> Block { + transactions.push(NSSATransaction::Public(clock_invocation( + id.saturating_mul(100), + ))); + let block_data = HashableBlockData { block_id: id, prev_block_hash: prev_hash.unwrap_or_default(), 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/flake.lock b/flake.lock index d0df80e3..1d9f4502 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "crane": { "locked": { - "lastModified": 1769737823, - "narHash": "sha256-DrBaNpZ+sJ4stXm+0nBX7zqZT9t9P22zbk6m5YhQxS4=", + "lastModified": 1776396856, + "narHash": "sha256-aRJpIJUlZLaf06ekPvqjuU46zvO9K90IxJGpbqodkPs=", "owner": "ipetkov", "repo": "crane", - "rev": "b2f45c3830aa96b7456a4c4bc327d04d7a43e1ba", + "rev": "28462d6d55c33206ffa5a56c7907ca3125ed788f", "type": "github" }, "original": { @@ -20,11 +20,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1770979891, - "narHash": "sha256-cvkVnE7btuFLzv70ORAZve9K1Huiplq0iECgXSXb0ZY=", + "lastModified": 1775835011, + "narHash": "sha256-SQDLyyRUa5J9QHjNiHbeZw4rQOZnTEo61TcaUpjtLBs=", "owner": "logos-blockchain", "repo": "logos-blockchain-circuits", - "rev": "ec7d298e5a3a0507bb8570df86cdf78dc452d024", + "rev": "d6cf41f66500d4afc157b4f43de0f0d5bfa01443", "type": "github" }, "original": { @@ -51,11 +51,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1770019141, - "narHash": "sha256-VKS4ZLNx4PNrABoB0L8KUpc1fE7CLpQXQs985tGfaCU=", + "lastModified": 1776169885, + "narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "cb369ef2efd432b3cdf8622b0ffc0a97a02f3137", + "rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9", "type": "github" }, "original": { @@ -80,11 +80,11 @@ ] }, "locked": { - "lastModified": 1770088046, - "narHash": "sha256-4hfYDnUTvL1qSSZEA4CEThxfz+KlwSFQ30Z9jgDguO0=", + "lastModified": 1776395632, + "narHash": "sha256-Mi1uF5f2FsdBIvy+v7MtsqxD3Xjhd0ARJdwoqqqPtJo=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "71f9daa4e05e49c434d08627e755495ae222bc34", + "rev": "8087ff1f47fff983a1fba70fa88b759f2fd8ae97", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index be20b56b..0b6ff35f 100644 --- a/flake.nix +++ b/flake.nix @@ -130,9 +130,26 @@ ''; } ); + + indexerFfiPackage = craneLib.buildPackage ( + commonArgs + // { + pname = "logos-execution-zone-indexer-ffi"; + version = "0.1.0"; + cargoExtraArgs = "-p indexer_ffi"; + postInstall = '' + mkdir -p $out/include + cp indexer_ffi/indexer_ffi.h $out/include/ + '' + + pkgs.lib.optionalString pkgs.stdenv.isDarwin '' + install_name_tool -id @rpath/libindexer_ffi.dylib $out/lib/libindexer_ffi.dylib + ''; + } + ); in { wallet = walletFfiPackage; + indexer = indexerFfiPackage; default = walletFfiPackage; } ); @@ -144,9 +161,14 @@ walletFfiShell = pkgs.mkShell { inputsFrom = [ walletFfiPackage ]; }; + indexerFfiPackage = self.packages.${system}.indexer; + indexerFfiShell = pkgs.mkShell { + inputsFrom = [ indexerFfiPackage ]; + }; in { wallet = walletFfiShell; + indexer = indexerFfiShell; default = walletFfiShell; } ); diff --git a/indexer/core/src/block_store.rs b/indexer/core/src/block_store.rs index 611dec8d..cff07b0f 100644 --- a/indexer/core/src/block_store.rs +++ b/indexer/core/src/block_store.rs @@ -243,14 +243,9 @@ mod tests { &sign_key, ); let block_id = u64::try_from(i).unwrap(); - let block_timestamp = block_id.saturating_mul(100); - let clock_tx = NSSATransaction::Public(clock_invocation(block_timestamp)); - let next_block = common::test_utils::produce_dummy_block( - block_id, - Some(prev_hash), - vec![tx, clock_tx], - ); + let next_block = + common::test_utils::produce_dummy_block(block_id, Some(prev_hash), vec![tx]); prev_hash = next_block.header.hash; storage diff --git a/indexer/core/src/lib.rs b/indexer/core/src/lib.rs index 10e0834a..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() @@ -143,23 +144,27 @@ impl IndexerCore { l2_blocks_parsed_ids.sort_unstable(); info!("Parsed {} L2 blocks with ids {:?}", l2_block_vec.len(), l2_blocks_parsed_ids); - for l2_block in l2_block_vec { - // TODO: proper fix is to make the sequencer's genesis include a - // trailing `clock_invocation(0)` (and have the indexer's - // `open_db_with_genesis` not pre-apply state transitions) so the - // inscribed genesis can flow through `put_block` like any other - // block. For now we skip re-applying it. - // - // The channel-start (block_id == 1) is the sequencer's genesis - // inscription that we re-discover during initial search. The - // indexer already has its own locally-constructed genesis in - // the store from `open_db_with_genesis`, so re-applying the - // inscribed copy is both redundant and would fail the strict - // block validation in `put_block` (the inscribed genesis lacks - // the trailing clock invocation). - if l2_block.header.block_id != 1 { - self.store.put_block(l2_block.clone(), l1_header).await?; - } + for l2_block in l2_block_vec { + // TODO: proper fix is to make the sequencer's genesis include a + // trailing `clock_invocation(0)` (and have the indexer's + // `open_db_with_genesis` not pre-apply state transitions) so the + // inscribed genesis can flow through `put_block` like any other + // block. For now we skip re-applying it. + // + // The channel-start (block_id == 1) is the sequencer's genesis + // inscription that we re-discover during initial search. The + // indexer already has its own locally-constructed genesis in + // the store from `open_db_with_genesis`, so re-applying the + // inscribed copy is both redundant and would fail the strict + // block validation in `put_block` (the inscribed genesis lacks + // the trailing clock invocation). + if l2_block.header.block_id != 1 { + self + .store + .put_block(l2_block.clone(), l1_header) + .await + .inspect_err(|err| error!("Failed to put block with err {err:?}"))?; + } yield Ok(l2_block); } diff --git a/indexer/service/src/mock_service.rs b/indexer/service/src/mock_service.rs index 09ae96f5..c4a099b8 100644 --- a/indexer/service/src/mock_service.rs +++ b/indexer/service/src/mock_service.rs @@ -6,7 +6,7 @@ clippy::integer_division_remainder_used, reason = "Mock service uses intentional casts and format patterns for test data generation" )] -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc, time::Duration}; use indexer_service_protocol::{ Account, AccountId, BedrockStatus, Block, BlockBody, BlockHeader, BlockId, Commitment, @@ -19,15 +19,73 @@ use jsonrpsee::{ core::{SubscriptionResult, async_trait}, types::ErrorObjectOwned, }; +use tokio::sync::{RwLock, broadcast}; -/// A mock implementation of the `IndexerService` RPC for testing purposes. -pub struct MockIndexerService { +const MOCK_GENESIS_TIMESTAMP_MS: u64 = 1_704_067_200_000; +const MOCK_BLOCK_INTERVAL_MS: u64 = 30_000; + +struct MockState { blocks: Vec, accounts: HashMap, + account_ids: Vec, transactions: HashMap, } +/// A mock implementation of the `IndexerService` RPC for testing purposes. +pub struct MockIndexerService { + state: Arc>, + finalized_blocks_tx: broadcast::Sender, +} + impl MockIndexerService { + fn spawn_block_generation_task( + state: Arc>, + finalized_blocks_tx: broadcast::Sender, + ) { + tokio::spawn(async move { + loop { + tokio::time::sleep(Duration::from_secs(30)).await; + + let new_block = { + let mut state = state.write().await; + + let next_block_id = state + .blocks + .last() + .map_or(1, |block| block.header.block_id.saturating_add(1)); + let prev_hash = state + .blocks + .last() + .map_or(HashType([0_u8; 32]), |block| block.header.hash); + let timestamp = state.blocks.last().map_or( + MOCK_GENESIS_TIMESTAMP_MS + MOCK_BLOCK_INTERVAL_MS, + |block| { + block + .header + .timestamp + .saturating_add(MOCK_BLOCK_INTERVAL_MS) + }, + ); + + let block = build_mock_block( + next_block_id, + prev_hash, + timestamp, + &state.account_ids, + BedrockStatus::Finalized, + ); + + index_block_transactions(&mut state.transactions, &block); + state.blocks.push(block.clone()); + + block + }; + + let _res = finalized_blocks_tx.send(new_block); + } + }); + } + #[must_use] pub fn new_with_mock_blocks() -> Self { let mut blocks = Vec::new(); @@ -59,119 +117,38 @@ impl MockIndexerService { let mut prev_hash = HashType([0_u8; 32]); for block_id in 1..=100 { - let block_hash = { - let mut hash = [0_u8; 32]; - hash[0] = block_id as u8; - hash[1] = 0xff; - HashType(hash) - }; - - // Create 2-4 transactions per block (mix of Public, PrivacyPreserving, and - // ProgramDeployment) - let num_txs = 2 + (block_id % 3); - let mut block_transactions = Vec::new(); - - for tx_idx in 0..num_txs { - let tx_hash = { - let mut hash = [0_u8; 32]; - hash[0] = block_id as u8; - hash[1] = tx_idx as u8; - HashType(hash) - }; - - // Vary transaction types: Public, PrivacyPreserving, or ProgramDeployment - let tx = match (block_id + tx_idx) % 5 { - // Public transactions (most common) - 0 | 1 => Transaction::Public(PublicTransaction { - hash: tx_hash, - message: PublicMessage { - program_id: ProgramId([1_u32; 8]), - account_ids: vec![ - account_ids[tx_idx as usize % account_ids.len()], - account_ids[(tx_idx as usize + 1) % account_ids.len()], - ], - nonces: vec![block_id as u128, (block_id + 1) as u128], - instruction_data: vec![1, 2, 3, 4], - }, - witness_set: WitnessSet { - signatures_and_public_keys: vec![], - proof: None, - }, - }), - // PrivacyPreserving transactions - 2 | 3 => Transaction::PrivacyPreserving(PrivacyPreservingTransaction { - hash: tx_hash, - message: PrivacyPreservingMessage { - public_account_ids: vec![ - account_ids[tx_idx as usize % account_ids.len()], - ], - nonces: vec![block_id as u128], - public_post_states: vec![Account { - program_owner: ProgramId([1_u32; 8]), - balance: 500, - data: Data(vec![0xdd, 0xee]), - nonce: block_id as u128, - }], - encrypted_private_post_states: vec![EncryptedAccountData { - ciphertext: indexer_service_protocol::Ciphertext(vec![ - 0x01, 0x02, 0x03, 0x04, - ]), - epk: indexer_service_protocol::EphemeralPublicKey(vec![0xaa; 32]), - view_tag: 42, - }], - new_commitments: vec![Commitment([block_id as u8; 32])], - new_nullifiers: vec![( - indexer_service_protocol::Nullifier([tx_idx as u8; 32]), - CommitmentSetDigest([0xff; 32]), - )], - block_validity_window: ValidityWindow((None, None)), - timestamp_validity_window: ValidityWindow((None, None)), - }, - witness_set: WitnessSet { - signatures_and_public_keys: vec![], - proof: Some(indexer_service_protocol::Proof(vec![0; 32])), - }, - }), - // ProgramDeployment transactions (rare) - _ => Transaction::ProgramDeployment(ProgramDeploymentTransaction { - hash: tx_hash, - message: ProgramDeploymentMessage { - bytecode: vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00], /* WASM magic number */ - }, - }), - }; - - transactions.insert(tx_hash, (tx.clone(), block_id)); - block_transactions.push(tx); - } - - let block = Block { - header: BlockHeader { - block_id, - prev_block_hash: prev_hash, - hash: block_hash, - timestamp: 1_704_067_200_000 + (block_id * 12_000), // ~12 seconds per block - signature: Signature([0_u8; 64]), - }, - body: BlockBody { - transactions: block_transactions, - }, - bedrock_status: match block_id { + let block = build_mock_block( + block_id, + prev_hash, + MOCK_GENESIS_TIMESTAMP_MS + (block_id * MOCK_BLOCK_INTERVAL_MS), + &account_ids, + match block_id { 0..=5 => BedrockStatus::Finalized, 6..=8 => BedrockStatus::Safe, _ => BedrockStatus::Pending, }, - bedrock_parent_id: MantleMsgId([0; 32]), - }; + ); - prev_hash = block_hash; + index_block_transactions(&mut transactions, &block); + + prev_hash = block.header.hash; blocks.push(block); } - Self { + let state = Arc::new(RwLock::new(MockState { blocks, accounts, + account_ids, transactions, + })); + + let (finalized_blocks_tx, _) = broadcast::channel(32); + + Self::spawn_block_generation_task(Arc::clone(&state), finalized_blocks_tx.clone()); + + Self { + state, + finalized_blocks_tx, } } } @@ -183,21 +160,45 @@ impl indexer_service_rpc::RpcServer for MockIndexerService { subscription_sink: jsonrpsee::PendingSubscriptionSink, ) -> SubscriptionResult { let sink = subscription_sink.accept().await?; - for block in self - .blocks - .iter() - .filter(|b| b.bedrock_status == BedrockStatus::Finalized) - { + let initial_finalized_blocks: Vec = { + let state = self.state.read().await; + state + .blocks + .iter() + .filter(|b| b.bedrock_status == BedrockStatus::Finalized) + .cloned() + .collect() + }; + + for block in &initial_finalized_blocks { let json = serde_json::value::to_raw_value(block).unwrap(); sink.send(json).await?; } + + let mut receiver = self.finalized_blocks_tx.subscribe(); + loop { + match receiver.recv().await { + Ok(block) => { + let json = serde_json::value::to_raw_value(&block).unwrap(); + sink.send(json).await?; + } + Err(broadcast::error::RecvError::Lagged(_)) => {} + Err(broadcast::error::RecvError::Closed) => break, + } + } + Ok(()) } async fn get_last_finalized_block_id(&self) -> Result { - self.blocks - .last() - .map(|bl| bl.header.block_id) + self.state + .read() + .await + .blocks + .iter() + .rev() + .find(|block| block.bedrock_status == BedrockStatus::Finalized) + .map(|block| block.header.block_id) .ok_or_else(|| { ErrorObjectOwned::owned(-32001, "Last block not found".to_owned(), None::<()>) }) @@ -205,6 +206,9 @@ impl indexer_service_rpc::RpcServer for MockIndexerService { async fn get_block_by_id(&self, block_id: BlockId) -> Result, ErrorObjectOwned> { Ok(self + .state + .read() + .await .blocks .iter() .find(|b| b.header.block_id == block_id) @@ -216,6 +220,9 @@ impl indexer_service_rpc::RpcServer for MockIndexerService { block_hash: HashType, ) -> Result, ErrorObjectOwned> { Ok(self + .state + .read() + .await .blocks .iter() .find(|b| b.header.hash == block_hash) @@ -223,7 +230,10 @@ impl indexer_service_rpc::RpcServer for MockIndexerService { } async fn get_account(&self, account_id: AccountId) -> Result { - self.accounts + self.state + .read() + .await + .accounts .get(&account_id) .cloned() .ok_or_else(|| ErrorObjectOwned::owned(-32001, "Account not found", None::<()>)) @@ -233,7 +243,13 @@ impl indexer_service_rpc::RpcServer for MockIndexerService { &self, tx_hash: HashType, ) -> Result, ErrorObjectOwned> { - Ok(self.transactions.get(&tx_hash).map(|(tx, _)| tx.clone())) + Ok(self + .state + .read() + .await + .transactions + .get(&tx_hash) + .map(|(tx, _)| tx.clone())) } async fn get_blocks( @@ -241,15 +257,17 @@ impl indexer_service_rpc::RpcServer for MockIndexerService { before: Option, limit: u64, ) -> Result, ErrorObjectOwned> { + let state = self.state.read().await; + let start_id = before.map_or_else( - || self.blocks.len(), + || state.blocks.len(), |id| usize::try_from(id.saturating_sub(1)).expect("u64 should fit in usize"), ); let result = (1..=start_id) .rev() .take(limit as usize) - .map_while(|block_id| self.blocks.get(block_id - 1).cloned()) + .map_while(|block_id| state.blocks.get(block_id - 1).cloned()) .collect(); Ok(result) @@ -261,20 +279,24 @@ impl indexer_service_rpc::RpcServer for MockIndexerService { offset: u64, limit: u64, ) -> Result, ErrorObjectOwned> { - let mut account_txs: Vec<_> = self - .transactions - .values() - .filter(|(tx, _)| match tx { - Transaction::Public(pub_tx) => pub_tx.message.account_ids.contains(&account_id), - Transaction::PrivacyPreserving(priv_tx) => { - priv_tx.message.public_account_ids.contains(&account_id) - } - Transaction::ProgramDeployment(_) => false, - }) - .collect(); + let mut account_txs: Vec<(Transaction, BlockId)> = { + let state = self.state.read().await; + state + .transactions + .values() + .filter(|(tx, _)| match tx { + Transaction::Public(pub_tx) => pub_tx.message.account_ids.contains(&account_id), + Transaction::PrivacyPreserving(priv_tx) => { + priv_tx.message.public_account_ids.contains(&account_id) + } + Transaction::ProgramDeployment(_) => false, + }) + .cloned() + .collect() + }; // Sort by block ID descending (most recent first) - account_txs.sort_by_key(|b| std::cmp::Reverse(b.1)); + account_txs.sort_by_key(|(_, block_id)| std::cmp::Reverse(*block_id)); let start = offset as usize; if start >= account_txs.len() { @@ -293,3 +315,123 @@ impl indexer_service_rpc::RpcServer for MockIndexerService { Ok(()) } } + +fn build_mock_block( + block_id: BlockId, + prev_hash: HashType, + timestamp: u64, + account_ids: &[AccountId], + bedrock_status: BedrockStatus, +) -> Block { + let block_hash = { + let mut hash = [0_u8; 32]; + hash[0] = block_id as u8; + hash[1] = 0xff; + HashType(hash) + }; + + // Create 2-4 transactions per block (mix of Public, PrivacyPreserving, and ProgramDeployment) + let num_txs = 2 + (block_id % 3); + let mut block_transactions = Vec::new(); + + for tx_idx in 0..num_txs { + let tx_hash = { + let mut hash = [0_u8; 32]; + hash[0] = block_id as u8; + hash[1] = tx_idx as u8; + HashType(hash) + }; + + // Vary transaction types: Public, PrivacyPreserving, or ProgramDeployment + let tx = match (block_id + tx_idx) % 5 { + // Public transactions (most common) + 0 | 1 => Transaction::Public(PublicTransaction { + hash: tx_hash, + message: PublicMessage { + program_id: ProgramId([1_u32; 8]), + account_ids: vec![ + account_ids[tx_idx as usize % account_ids.len()], + account_ids[(tx_idx as usize + 1) % account_ids.len()], + ], + nonces: vec![block_id as u128, (block_id + 1) as u128], + instruction_data: vec![1, 2, 3, 4], + }, + witness_set: WitnessSet { + signatures_and_public_keys: vec![], + proof: None, + }, + }), + // PrivacyPreserving transactions + 2 | 3 => Transaction::PrivacyPreserving(PrivacyPreservingTransaction { + hash: tx_hash, + message: PrivacyPreservingMessage { + public_account_ids: vec![account_ids[tx_idx as usize % account_ids.len()]], + nonces: vec![block_id as u128], + public_post_states: vec![Account { + program_owner: ProgramId([1_u32; 8]), + balance: 500, + data: Data(vec![0xdd, 0xee]), + nonce: block_id as u128, + }], + encrypted_private_post_states: vec![EncryptedAccountData { + ciphertext: indexer_service_protocol::Ciphertext(vec![ + 0x01, 0x02, 0x03, 0x04, + ]), + epk: indexer_service_protocol::EphemeralPublicKey(vec![0xaa; 32]), + view_tag: 42, + }], + new_commitments: vec![Commitment([block_id as u8; 32])], + new_nullifiers: vec![( + indexer_service_protocol::Nullifier([tx_idx as u8; 32]), + CommitmentSetDigest([0xff; 32]), + )], + block_validity_window: ValidityWindow((None, None)), + timestamp_validity_window: ValidityWindow((None, None)), + }, + witness_set: WitnessSet { + signatures_and_public_keys: vec![], + proof: Some(indexer_service_protocol::Proof(vec![0; 32])), + }, + }), + // ProgramDeployment transactions (rare) + _ => Transaction::ProgramDeployment(ProgramDeploymentTransaction { + hash: tx_hash, + message: ProgramDeploymentMessage { + bytecode: vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00], /* WASM magic + * number */ + }, + }), + }; + + block_transactions.push(tx); + } + + Block { + header: BlockHeader { + block_id, + prev_block_hash: prev_hash, + hash: block_hash, + timestamp, + signature: Signature([0_u8; 64]), + }, + body: BlockBody { + transactions: block_transactions, + }, + bedrock_status, + bedrock_parent_id: MantleMsgId([0; 32]), + } +} + +fn index_block_transactions( + transactions: &mut HashMap, + block: &Block, +) { + for tx in &block.body.transactions { + let tx_hash = match tx { + Transaction::Public(public_tx) => public_tx.hash, + Transaction::PrivacyPreserving(private_tx) => private_tx.hash, + Transaction::ProgramDeployment(deployment_tx) => deployment_tx.hash, + }; + transactions.insert(tx_hash, (tx.clone(), block.header.block_id)); + } +} diff --git a/indexer_ffi/Cargo.toml b/indexer_ffi/Cargo.toml new file mode 100644 index 00000000..b55230c6 --- /dev/null +++ b/indexer_ffi/Cargo.toml @@ -0,0 +1,25 @@ +[package] +edition = "2024" +license = { workspace = true } +name = "indexer_ffi" +version = "0.1.0" + +[dependencies] +indexer_service.workspace = true +log = { workspace = true } +tokio = { features = ["rt-multi-thread"], workspace = true } + +[build-dependencies] +cbindgen = "0.29" + +[lib] +crate-type = ["rlib", "cdylib", "staticlib"] +name = "indexer_ffi" + +[lints] +workspace = true + +[package.metadata.cargo-machete] +ignored = [ + "cbindgen", +] # machete does not recognize this for build dep and complains. diff --git a/indexer_ffi/build.rs b/indexer_ffi/build.rs new file mode 100644 index 00000000..92c95407 --- /dev/null +++ b/indexer_ffi/build.rs @@ -0,0 +1,12 @@ +use std::env; + +fn main() { + let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + println!("cargo:rerun-if-changed=src/"); + cbindgen::Builder::new() + .with_crate(crate_dir) + .with_language(cbindgen::Language::C) + .generate() + .expect("Unable to generate bindings") + .write_to_file("indexer_ffi.h"); +} diff --git a/indexer_ffi/cbindgen.toml b/indexer_ffi/cbindgen.toml new file mode 100644 index 00000000..79f622b7 --- /dev/null +++ b/indexer_ffi/cbindgen.toml @@ -0,0 +1,2 @@ +language = "C" # For increased compatibility +no_includes = true diff --git a/indexer_ffi/indexer_ffi.h b/indexer_ffi/indexer_ffi.h new file mode 100644 index 00000000..7c7d9a4d --- /dev/null +++ b/indexer_ffi/indexer_ffi.h @@ -0,0 +1,76 @@ +#include +#include +#include +#include + +typedef enum OperationStatus { + Ok = 0, + NullPointer = 1, + InitializationError = 2, +} OperationStatus; + +typedef struct IndexerServiceFFI { + void *indexer_handle; + void *runtime; +} IndexerServiceFFI; + +/** + * Simple wrapper around a pointer to a value or an error. + * + * Pointer is not guaranteed. You should check the error field before + * dereferencing the pointer. + */ +typedef struct PointerResult_IndexerServiceFFI__OperationStatus { + struct IndexerServiceFFI *value; + enum OperationStatus error; +} PointerResult_IndexerServiceFFI__OperationStatus; + +typedef struct PointerResult_IndexerServiceFFI__OperationStatus InitializedIndexerServiceFFIResult; + +/** + * Creates and starts an indexer based on the provided + * configuration file path. + * + * # Arguments + * + * - `config_path`: A pointer to a string representing the path to the configuration file. + * - `port`: Number representing a port, on which indexers RPC will start. + * + * # Returns + * + * An `InitializedIndexerServiceFFIResult` containing either a pointer to the + * initialized `IndexerServiceFFI` or an error code. + */ +InitializedIndexerServiceFFIResult start_indexer(const char *config_path, uint16_t port); + +/** + * Stops and frees the resources associated with the given indexer service. + * + * # Arguments + * + * - `indexer`: A pointer to the `IndexerServiceFFI` instance to be stopped. + * + * # Returns + * + * An `OperationStatus` indicating success or failure. + * + * # Safety + * + * The caller must ensure that: + * - `indexer` is a valid pointer to a `IndexerServiceFFI` instance + * - The `IndexerServiceFFI` instance was created by this library + * - The pointer will not be used after this function returns + */ +enum OperationStatus stop_indexer(struct IndexerServiceFFI *indexer); + +/** + * # Safety + * It's up to the caller to pass a proper pointer, if somehow from c/c++ side + * this is called with a type which doesn't come from a returned `CString` it + * will cause a segfault. + */ +void free_cstring(char *block); + +bool is_ok(const enum OperationStatus *self); + +bool is_error(const enum OperationStatus *self); diff --git a/indexer_ffi/src/api/lifecycle.rs b/indexer_ffi/src/api/lifecycle.rs new file mode 100644 index 00000000..735efd4d --- /dev/null +++ b/indexer_ffi/src/api/lifecycle.rs @@ -0,0 +1,100 @@ +use std::{ffi::c_char, path::PathBuf}; + +use tokio::runtime::Runtime; + +use crate::{IndexerServiceFFI, api::PointerResult, errors::OperationStatus}; + +pub type InitializedIndexerServiceFFIResult = PointerResult; + +/// Creates and starts an indexer based on the provided +/// configuration file path. +/// +/// # Arguments +/// +/// - `config_path`: A pointer to a string representing the path to the configuration file. +/// - `port`: Number representing a port, on which indexers RPC will start. +/// +/// # Returns +/// +/// An `InitializedIndexerServiceFFIResult` containing either a pointer to the +/// initialized `IndexerServiceFFI` or an error code. +#[unsafe(no_mangle)] +pub extern "C" fn start_indexer( + config_path: *const c_char, + port: u16, +) -> InitializedIndexerServiceFFIResult { + setup_indexer(config_path, port).map_or_else( + InitializedIndexerServiceFFIResult::from_error, + InitializedIndexerServiceFFIResult::from_value, + ) +} + +/// Initializes and starts an indexer based on the provided +/// configuration file path. +/// +/// # Arguments +/// +/// - `config_path`: A pointer to a string representing the path to the configuration file. +/// - `port`: Number representing a port, on which indexers RPC will start. +/// +/// # Returns +/// +/// A `Result` containing either the initialized `IndexerServiceFFI` or an +/// error code. +fn setup_indexer( + config_path: *const c_char, + port: u16, +) -> Result { + let user_config_path = PathBuf::from( + unsafe { std::ffi::CStr::from_ptr(config_path) } + .to_str() + .map_err(|e| { + log::error!("Could not convert the config path to string: {e}"); + OperationStatus::InitializationError + })?, + ); + let config = indexer_service::IndexerConfig::from_path(&user_config_path).map_err(|e| { + log::error!("Failed to read config: {e}"); + OperationStatus::InitializationError + })?; + + let rt = Runtime::new().unwrap(); + + let indexer_handle = rt + .block_on(indexer_service::run_server(config, port)) + .map_err(|e| { + log::error!("Could not start indexer service: {e}"); + OperationStatus::InitializationError + })?; + + Ok(IndexerServiceFFI::new(indexer_handle, rt)) +} + +/// Stops and frees the resources associated with the given indexer service. +/// +/// # Arguments +/// +/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be stopped. +/// +/// # Returns +/// +/// An `OperationStatus` indicating success or failure. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance +/// - The `IndexerServiceFFI` instance was created by this library +/// - The pointer will not be used after this function returns +#[unsafe(no_mangle)] +pub unsafe extern "C" fn stop_indexer(indexer: *mut IndexerServiceFFI) -> OperationStatus { + if indexer.is_null() { + log::error!("Attempted to stop a null indexer pointer. This is a bug. Aborting."); + return OperationStatus::NullPointer; + } + + let indexer = unsafe { Box::from_raw(indexer) }; + drop(indexer); + + OperationStatus::Ok +} diff --git a/indexer_ffi/src/api/memory.rs b/indexer_ffi/src/api/memory.rs new file mode 100644 index 00000000..f266d309 --- /dev/null +++ b/indexer_ffi/src/api/memory.rs @@ -0,0 +1,14 @@ +use std::ffi::{CString, c_char}; + +/// # Safety +/// It's up to the caller to pass a proper pointer, if somehow from c/c++ side +/// this is called with a type which doesn't come from a returned `CString` it +/// will cause a segfault. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn free_cstring(block: *mut c_char) { + if block.is_null() { + log::error!("Trying to free a null pointer. Exiting"); + return; + } + drop(unsafe { CString::from_raw(block) }); +} diff --git a/indexer_ffi/src/api/mod.rs b/indexer_ffi/src/api/mod.rs new file mode 100644 index 00000000..e84a3913 --- /dev/null +++ b/indexer_ffi/src/api/mod.rs @@ -0,0 +1,5 @@ +pub use result::PointerResult; + +pub mod lifecycle; +pub mod memory; +pub mod result; diff --git a/indexer_ffi/src/api/result.rs b/indexer_ffi/src/api/result.rs new file mode 100644 index 00000000..96cbcdd8 --- /dev/null +++ b/indexer_ffi/src/api/result.rs @@ -0,0 +1,29 @@ +/// Simple wrapper around a pointer to a value or an error. +/// +/// Pointer is not guaranteed. You should check the error field before +/// dereferencing the pointer. +#[repr(C)] +pub struct PointerResult { + pub value: *mut Type, + pub error: Error, +} + +impl PointerResult { + pub fn from_pointer(pointer: *mut Type) -> Self { + Self { + value: pointer, + error: Error::default(), + } + } + + pub fn from_value(value: Type) -> Self { + Self::from_pointer(Box::into_raw(Box::new(value))) + } + + pub const fn from_error(error: Error) -> Self { + Self { + value: std::ptr::null_mut(), + error, + } + } +} diff --git a/indexer_ffi/src/errors.rs b/indexer_ffi/src/errors.rs new file mode 100644 index 00000000..46aa0f9f --- /dev/null +++ b/indexer_ffi/src/errors.rs @@ -0,0 +1,22 @@ +#[derive(Debug, Default, PartialEq, Eq)] +#[repr(C)] +pub enum OperationStatus { + #[default] + Ok = 0x0, + NullPointer = 0x1, + InitializationError = 0x2, +} + +impl OperationStatus { + #[must_use] + #[unsafe(no_mangle)] + pub extern "C" fn is_ok(&self) -> bool { + *self == Self::Ok + } + + #[must_use] + #[unsafe(no_mangle)] + pub extern "C" fn is_error(&self) -> bool { + !self.is_ok() + } +} diff --git a/indexer_ffi/src/indexer.rs b/indexer_ffi/src/indexer.rs new file mode 100644 index 00000000..c110b183 --- /dev/null +++ b/indexer_ffi/src/indexer.rs @@ -0,0 +1,86 @@ +use std::{ffi::c_void, net::SocketAddr}; + +use indexer_service::IndexerHandle; +use tokio::runtime::Runtime; + +#[repr(C)] +pub struct IndexerServiceFFI { + indexer_handle: *mut c_void, + runtime: *mut c_void, +} + +impl IndexerServiceFFI { + pub fn new(indexer_handle: indexer_service::IndexerHandle, runtime: Runtime) -> Self { + Self { + // Box the complex types and convert to opaque pointers + indexer_handle: Box::into_raw(Box::new(indexer_handle)).cast::(), + runtime: Box::into_raw(Box::new(runtime)).cast::(), + } + } + + /// Helper to take ownership back. + /// + /// # Safety + /// + /// The caller must ensure that: + /// - `self` is a valid object(contains valid pointers in all fields) + #[must_use] + pub unsafe fn into_parts(self) -> (Box, Box) { + let indexer_handle = unsafe { Box::from_raw(self.indexer_handle.cast::()) }; + let runtime = unsafe { Box::from_raw(self.runtime.cast::()) }; + (indexer_handle, runtime) + } + + /// Helper to get indexer handle addr. + /// + /// # Safety + /// + /// The caller must ensure that: + /// - `self` is a valid object(contains valid pointers in all fields) + #[must_use] + pub const unsafe fn addr(&self) -> SocketAddr { + let indexer_handle = unsafe { + self.indexer_handle + .cast::() + .as_ref() + .expect("Indexer Handle must be non-null pointer") + }; + + indexer_handle.addr() + } + + /// Helper to get indexer handle addr. + /// + /// # Safety + /// + /// The caller must ensure that: + /// - `self` is a valid object(contains valid pointers in all fields) + #[must_use] + pub const unsafe fn handle(&self) -> &IndexerHandle { + unsafe { + self.indexer_handle + .cast::() + .as_ref() + .expect("Indexer Handle must be non-null pointer") + } + } +} + +// Implement Drop to prevent memory leaks +impl Drop for IndexerServiceFFI { + fn drop(&mut self) { + let Self { + indexer_handle, + runtime, + } = self; + + if indexer_handle.is_null() { + log::error!("Attempted to drop a null indexer pointer. This is a bug"); + } + if runtime.is_null() { + log::error!("Attempted to drop a null tokio runtime pointer. This is a bug"); + } + drop(unsafe { Box::from_raw(indexer_handle.cast::()) }); + drop(unsafe { Box::from_raw(runtime.cast::()) }); + } +} diff --git a/indexer_ffi/src/lib.rs b/indexer_ffi/src/lib.rs new file mode 100644 index 00000000..fe594ec0 --- /dev/null +++ b/indexer_ffi/src/lib.rs @@ -0,0 +1,8 @@ +#![allow(clippy::undocumented_unsafe_blocks, reason = "It is an FFI")] + +pub use errors::OperationStatus; +pub use indexer::IndexerServiceFFI; + +pub mod api; +mod errors; +mod indexer; diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index cb5277d2..53f0ee98 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -22,6 +22,7 @@ ata_core.workspace = true indexer_service_rpc.workspace = true sequencer_service_rpc = { workspace = true, features = ["client"] } wallet-ffi.workspace = true +indexer_ffi.workspace = true testnet_initial_state.workspace = true url.workspace = true 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/src/lib.rs b/integration_tests/src/lib.rs index a4381acf..fcae2c71 100644 --- a/integration_tests/src/lib.rs +++ b/integration_tests/src/lib.rs @@ -1,12 +1,12 @@ //! This library contains common code for integration tests. -use std::{net::SocketAddr, path::PathBuf, sync::LazyLock}; +use std::sync::LazyLock; -use anyhow::{Context as _, Result, bail}; +use anyhow::{Context as _, Result}; use common::{HashType, transaction::NSSATransaction}; use futures::FutureExt as _; use indexer_service::IndexerHandle; -use log::{debug, error, warn}; +use log::{debug, error}; use nssa::{AccountId, PrivacyPreservingTransaction}; use nssa_core::Commitment; use sequencer_core::indexer_client::{IndexerClient, IndexerClientTrait as _}; @@ -14,9 +14,13 @@ use sequencer_service::SequencerHandle; use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder}; use tempfile::TempDir; use testcontainers::compose::DockerCompose; -use wallet::{WalletCore, config::WalletConfigOverrides}; +use wallet::WalletCore; + +use crate::setup::{setup_bedrock_node, setup_indexer, setup_sequencer, setup_wallet}; pub mod config; +pub mod setup; +pub mod test_context_ffi; // TODO: Remove this and control time from tests pub const TIME_TO_WAIT_FOR_BLOCK_SECONDS: u64 = 12; @@ -67,13 +71,13 @@ impl TestContext { debug!("Test context setup"); - let (bedrock_compose, bedrock_addr) = Self::setup_bedrock_node().await?; + let (bedrock_compose, bedrock_addr) = setup_bedrock_node().await?; - let (indexer_handle, temp_indexer_dir) = Self::setup_indexer(bedrock_addr, &initial_data) + let (indexer_handle, temp_indexer_dir) = setup_indexer(bedrock_addr, &initial_data) .await .context("Failed to setup Indexer")?; - let (sequencer_handle, temp_sequencer_dir) = Self::setup_sequencer( + let (sequencer_handle, temp_sequencer_dir) = setup_sequencer( sequencer_partial_config, bedrock_addr, indexer_handle.addr(), @@ -83,7 +87,7 @@ impl TestContext { .context("Failed to setup Sequencer")?; let (wallet, temp_wallet_dir, wallet_password) = - Self::setup_wallet(sequencer_handle.addr(), &initial_data) + setup_wallet(sequencer_handle.addr(), &initial_data) .await .context("Failed to setup wallet")?; @@ -112,165 +116,6 @@ impl TestContext { }) } - async fn setup_bedrock_node() -> Result<(DockerCompose, SocketAddr)> { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - let bedrock_compose_path = - PathBuf::from(manifest_dir).join("../bedrock/docker-compose.yml"); - - let mut compose = DockerCompose::with_auto_client(&[bedrock_compose_path]) - .await - .context("Failed to setup docker compose for Bedrock")? - // Setting port to 0 to avoid conflicts between parallel tests, actual port will be retrieved after container is up - .with_env("PORT", "0"); - - #[expect( - clippy::items_after_statements, - reason = "This is more readable is this function used just after its definition" - )] - async fn up_and_retrieve_port(compose: &mut DockerCompose) -> Result { - compose - .up() - .await - .context("Failed to bring up Bedrock services")?; - let container = compose - .service(BEDROCK_SERVICE_WITH_OPEN_PORT) - .with_context(|| { - format!( - "Failed to get Bedrock service container `{BEDROCK_SERVICE_WITH_OPEN_PORT}`" - ) - })?; - - let ports = container.ports().await.with_context(|| { - format!( - "Failed to get ports for Bedrock service container `{}`", - container.id() - ) - })?; - ports - .map_to_host_port_ipv4(BEDROCK_SERVICE_PORT) - .with_context(|| { - format!( - "Failed to retrieve host port of {BEDROCK_SERVICE_PORT} container \ - port for container `{}`, existing ports: {ports:?}", - container.id() - ) - }) - } - - let mut port = None; - let mut attempt = 0_u32; - let max_attempts = 5_u32; - while port.is_none() && attempt < max_attempts { - attempt = attempt - .checked_add(1) - .expect("We check that attempt < max_attempts, so this won't overflow"); - match up_and_retrieve_port(&mut compose).await { - Ok(p) => { - port = Some(p); - } - Err(err) => { - warn!( - "Failed to bring up Bedrock services: {err:?}, attempt {attempt}/{max_attempts}" - ); - } - } - } - let Some(port) = port else { - bail!("Failed to bring up Bedrock services after {max_attempts} attempts"); - }; - - let addr = SocketAddr::from(([127, 0, 0, 1], port)); - Ok((compose, addr)) - } - - async fn setup_indexer( - bedrock_addr: SocketAddr, - initial_data: &config::InitialData, - ) -> Result<(IndexerHandle, TempDir)> { - let temp_indexer_dir = - tempfile::tempdir().context("Failed to create temp dir for indexer home")?; - - debug!( - "Using temp indexer home at {}", - temp_indexer_dir.path().display() - ); - - let indexer_config = config::indexer_config( - bedrock_addr, - temp_indexer_dir.path().to_owned(), - initial_data, - ) - .context("Failed to create Indexer config")?; - - indexer_service::run_server(indexer_config, 0) - .await - .context("Failed to run Indexer Service") - .map(|handle| (handle, temp_indexer_dir)) - } - - async fn setup_sequencer( - partial: config::SequencerPartialConfig, - bedrock_addr: SocketAddr, - indexer_addr: SocketAddr, - initial_data: &config::InitialData, - ) -> Result<(SequencerHandle, TempDir)> { - let temp_sequencer_dir = - tempfile::tempdir().context("Failed to create temp dir for sequencer home")?; - - debug!( - "Using temp sequencer home at {}", - temp_sequencer_dir.path().display() - ); - - let config = config::sequencer_config( - partial, - temp_sequencer_dir.path().to_owned(), - bedrock_addr, - indexer_addr, - initial_data, - ) - .context("Failed to create Sequencer config")?; - - let sequencer_handle = sequencer_service::run(config, 0).await?; - - Ok((sequencer_handle, temp_sequencer_dir)) - } - - async fn setup_wallet( - sequencer_addr: SocketAddr, - initial_data: &config::InitialData, - ) -> Result<(WalletCore, TempDir, String)> { - let config = config::wallet_config(sequencer_addr, initial_data) - .context("Failed to create Wallet config")?; - let config_serialized = - serde_json::to_string_pretty(&config).context("Failed to serialize Wallet config")?; - - let temp_wallet_dir = - tempfile::tempdir().context("Failed to create temp dir for wallet home")?; - - let config_path = temp_wallet_dir.path().join("wallet_config.json"); - std::fs::write(&config_path, config_serialized) - .context("Failed to write wallet config in temp dir")?; - - let storage_path = temp_wallet_dir.path().join("storage.json"); - let config_overrides = WalletConfigOverrides::default(); - - let wallet_password = "test_pass".to_owned(); - let (wallet, _mnemonic) = WalletCore::new_init_storage( - config_path, - storage_path, - Some(config_overrides), - &wallet_password, - ) - .context("Failed to init wallet")?; - wallet - .store_persistent_data() - .await - .context("Failed to store wallet persistent data")?; - - Ok((wallet, temp_wallet_dir, wallet_password)) - } - /// Get reference to the wallet. #[must_use] pub const fn wallet(&self) -> &WalletCore { diff --git a/integration_tests/src/setup.rs b/integration_tests/src/setup.rs new file mode 100644 index 00000000..58b33c60 --- /dev/null +++ b/integration_tests/src/setup.rs @@ -0,0 +1,220 @@ +use std::{ + ffi::{CString, c_char}, + fs::File, + io::Write as _, + net::SocketAddr, + path::PathBuf, +}; + +use anyhow::{Context as _, Result, bail}; +use indexer_ffi::{IndexerServiceFFI, api::lifecycle::InitializedIndexerServiceFFIResult}; +use indexer_service::IndexerHandle; +use log::{debug, warn}; +use sequencer_service::SequencerHandle; +use tempfile::TempDir; +use testcontainers::compose::DockerCompose; +use wallet::{WalletCore, config::WalletConfigOverrides}; + +use crate::{BEDROCK_SERVICE_PORT, BEDROCK_SERVICE_WITH_OPEN_PORT, config}; + +unsafe extern "C" { + fn start_indexer(config_path: *const c_char, port: u16) -> InitializedIndexerServiceFFIResult; +} + +pub(crate) async fn setup_bedrock_node() -> Result<(DockerCompose, SocketAddr)> { + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + let bedrock_compose_path = PathBuf::from(manifest_dir).join("../bedrock/docker-compose.yml"); + + let mut compose = DockerCompose::with_auto_client(&[bedrock_compose_path]) + .await + .context("Failed to setup docker compose for Bedrock")? + // Setting port to 0 to avoid conflicts between parallel tests, actual port will be retrieved after container is up + .with_env("PORT", "0"); + + #[expect( + clippy::items_after_statements, + reason = "This is more readable is this function used just after its definition" + )] + async fn up_and_retrieve_port(compose: &mut DockerCompose) -> Result { + compose + .up() + .await + .context("Failed to bring up Bedrock services")?; + let container = compose + .service(BEDROCK_SERVICE_WITH_OPEN_PORT) + .with_context(|| { + format!( + "Failed to get Bedrock service container `{BEDROCK_SERVICE_WITH_OPEN_PORT}`" + ) + })?; + + let ports = container.ports().await.with_context(|| { + format!( + "Failed to get ports for Bedrock service container `{}`", + container.id() + ) + })?; + ports + .map_to_host_port_ipv4(BEDROCK_SERVICE_PORT) + .with_context(|| { + format!( + "Failed to retrieve host port of {BEDROCK_SERVICE_PORT} container \ + port for container `{}`, existing ports: {ports:?}", + container.id() + ) + }) + } + + let mut port = None; + let mut attempt = 0_u32; + let max_attempts = 5_u32; + while port.is_none() && attempt < max_attempts { + attempt = attempt + .checked_add(1) + .expect("We check that attempt < max_attempts, so this won't overflow"); + match up_and_retrieve_port(&mut compose).await { + Ok(p) => { + port = Some(p); + } + Err(err) => { + warn!( + "Failed to bring up Bedrock services: {err:?}, attempt {attempt}/{max_attempts}" + ); + } + } + } + let Some(port) = port else { + bail!("Failed to bring up Bedrock services after {max_attempts} attempts"); + }; + + let addr = SocketAddr::from(([127, 0, 0, 1], port)); + Ok((compose, addr)) +} + +pub(crate) async fn setup_indexer( + bedrock_addr: SocketAddr, + initial_data: &config::InitialData, +) -> Result<(IndexerHandle, TempDir)> { + let temp_indexer_dir = + tempfile::tempdir().context("Failed to create temp dir for indexer home")?; + + debug!( + "Using temp indexer home at {}", + temp_indexer_dir.path().display() + ); + + let indexer_config = config::indexer_config( + bedrock_addr, + temp_indexer_dir.path().to_owned(), + initial_data, + ) + .context("Failed to create Indexer config")?; + + indexer_service::run_server(indexer_config, 0) + .await + .context("Failed to run Indexer Service") + .map(|handle| (handle, temp_indexer_dir)) +} + +pub(crate) async fn setup_sequencer( + partial: config::SequencerPartialConfig, + bedrock_addr: SocketAddr, + indexer_addr: SocketAddr, + initial_data: &config::InitialData, +) -> Result<(SequencerHandle, TempDir)> { + let temp_sequencer_dir = + tempfile::tempdir().context("Failed to create temp dir for sequencer home")?; + + debug!( + "Using temp sequencer home at {}", + temp_sequencer_dir.path().display() + ); + + let config = config::sequencer_config( + partial, + temp_sequencer_dir.path().to_owned(), + bedrock_addr, + indexer_addr, + initial_data, + ) + .context("Failed to create Sequencer config")?; + + let sequencer_handle = sequencer_service::run(config, 0).await?; + + Ok((sequencer_handle, temp_sequencer_dir)) +} + +pub(crate) async fn setup_wallet( + sequencer_addr: SocketAddr, + initial_data: &config::InitialData, +) -> Result<(WalletCore, TempDir, String)> { + let config = config::wallet_config(sequencer_addr, initial_data) + .context("Failed to create Wallet config")?; + let config_serialized = + serde_json::to_string_pretty(&config).context("Failed to serialize Wallet config")?; + + let temp_wallet_dir = + tempfile::tempdir().context("Failed to create temp dir for wallet home")?; + + let config_path = temp_wallet_dir.path().join("wallet_config.json"); + std::fs::write(&config_path, config_serialized) + .context("Failed to write wallet config in temp dir")?; + + let storage_path = temp_wallet_dir.path().join("storage.json"); + let config_overrides = WalletConfigOverrides::default(); + + let wallet_password = "test_pass".to_owned(); + let (wallet, _mnemonic) = WalletCore::new_init_storage( + config_path, + storage_path, + Some(config_overrides), + &wallet_password, + ) + .context("Failed to init wallet")?; + wallet + .store_persistent_data() + .await + .context("Failed to store wallet persistent data")?; + + Ok((wallet, temp_wallet_dir, wallet_password)) +} + +pub(crate) fn setup_indexer_ffi( + bedrock_addr: SocketAddr, + initial_data: &config::InitialData, +) -> Result<(IndexerServiceFFI, TempDir)> { + let temp_indexer_dir = + tempfile::tempdir().context("Failed to create temp dir for indexer home")?; + + debug!( + "Using temp indexer home at {}", + temp_indexer_dir.path().display() + ); + + let indexer_config = config::indexer_config( + bedrock_addr, + temp_indexer_dir.path().to_owned(), + initial_data, + ) + .context("Failed to create Indexer config")?; + + let config_json = serde_json::to_vec(&indexer_config)?; + let config_path = temp_indexer_dir.path().join("indexer_config.json"); + let mut file = File::create(config_path.as_path())?; + file.write_all(&config_json)?; + file.flush()?; + + let res = + // SAFETY: lib function ensures validity of value. + unsafe { start_indexer(CString::new(config_path.to_str().unwrap())?.as_ptr(), 0) }; + + if res.error.is_error() { + anyhow::bail!("Indexer FFI error {:?}", res.error); + } + + Ok(( + // SAFETY: lib function ensures validity of value. + unsafe { std::ptr::read(res.value) }, + temp_indexer_dir, + )) +} diff --git a/integration_tests/src/test_context_ffi.rs b/integration_tests/src/test_context_ffi.rs new file mode 100644 index 00000000..7d21aa28 --- /dev/null +++ b/integration_tests/src/test_context_ffi.rs @@ -0,0 +1,296 @@ +use std::sync::Arc; + +use anyhow::{Context as _, Result}; +use futures::FutureExt as _; +use indexer_ffi::IndexerServiceFFI; +use indexer_service_rpc::RpcClient as _; +use log::{debug, error}; +use nssa::AccountId; +use sequencer_core::indexer_client::{IndexerClient, IndexerClientTrait as _}; +use sequencer_service::SequencerHandle; +use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder}; +use tempfile::TempDir; +use testcontainers::compose::DockerCompose; +use wallet::WalletCore; + +use crate::{ + BEDROCK_SERVICE_WITH_OPEN_PORT, LOGGER, TestContextBuilder, config, + setup::{setup_bedrock_node, setup_indexer_ffi, setup_sequencer, setup_wallet}, +}; + +/// Test context which sets up a sequencer, indexer through ffi and a wallet for integration tests. +/// +/// It's memory and logically safe to create multiple instances of this struct in parallel tests, +/// as each instance uses its own temporary directories for sequencer and wallet data. +// NOTE: Order of fields is important for proper drop order. +pub struct TestContextFFI { + sequencer_client: SequencerClient, + indexer_client: IndexerClient, + wallet: WalletCore, + wallet_password: String, + /// Optional to move out value in Drop. + sequencer_handle: Option, + bedrock_compose: DockerCompose, + _temp_indexer_dir: TempDir, + _temp_sequencer_dir: TempDir, + _temp_wallet_dir: TempDir, +} + +#[expect( + clippy::multiple_inherent_impl, + reason = "It is more natural to have this implementation here" +)] +impl TestContextBuilder { + pub fn build_ffi( + self, + runtime: &Arc, + ) -> Result<(TestContextFFI, IndexerServiceFFI)> { + TestContextFFI::new_configured( + self.sequencer_partial_config.unwrap_or_default(), + &self.initial_data.unwrap_or_else(|| { + config::InitialData::with_two_public_and_two_private_initialized_accounts() + }), + runtime, + ) + } +} + +impl TestContextFFI { + /// Create new test context. + pub fn new(runtime: &Arc) -> Result<(Self, IndexerServiceFFI)> { + Self::builder().build_ffi(runtime) + } + + #[must_use] + pub const fn builder() -> TestContextBuilder { + TestContextBuilder::new() + } + + fn new_configured( + sequencer_partial_config: config::SequencerPartialConfig, + initial_data: &config::InitialData, + runtime: &Arc, + ) -> Result<(Self, IndexerServiceFFI)> { + // Ensure logger is initialized only once + *LOGGER; + + debug!("Test context setup"); + + let (bedrock_compose, bedrock_addr) = runtime.block_on(setup_bedrock_node())?; + + let (indexer_ffi, temp_indexer_dir) = + setup_indexer_ffi(bedrock_addr, initial_data).context("Failed to setup Indexer")?; + + let (sequencer_handle, temp_sequencer_dir) = runtime + .block_on(setup_sequencer( + sequencer_partial_config, + bedrock_addr, + // SAFETY: addr is valid if indexer_ffi is valid. + unsafe { indexer_ffi.addr() }, + initial_data, + )) + .context("Failed to setup Sequencer")?; + + let (wallet, temp_wallet_dir, wallet_password) = runtime + .block_on(setup_wallet(sequencer_handle.addr(), initial_data)) + .context("Failed to setup wallet")?; + + let sequencer_url = config::addr_to_url(config::UrlProtocol::Http, sequencer_handle.addr()) + .context("Failed to convert sequencer addr to URL")?; + let indexer_url = config::addr_to_url( + config::UrlProtocol::Ws, + // SAFETY: addr is valid if indexer_ffi is valid. + unsafe { indexer_ffi.addr() }, + ) + .context("Failed to convert indexer addr to URL")?; + let sequencer_client = SequencerClientBuilder::default() + .build(sequencer_url) + .context("Failed to create sequencer client")?; + let indexer_client = runtime + .block_on(IndexerClient::new(&indexer_url)) + .context("Failed to create indexer client")?; + + Ok(( + Self { + sequencer_client, + indexer_client, + wallet, + wallet_password, + bedrock_compose, + sequencer_handle: Some(sequencer_handle), + _temp_indexer_dir: temp_indexer_dir, + _temp_sequencer_dir: temp_sequencer_dir, + _temp_wallet_dir: temp_wallet_dir, + }, + indexer_ffi, + )) + } + + /// Get reference to the wallet. + #[must_use] + pub const fn wallet(&self) -> &WalletCore { + &self.wallet + } + + #[must_use] + pub fn wallet_password(&self) -> &str { + &self.wallet_password + } + + /// Get mutable reference to the wallet. + pub const fn wallet_mut(&mut self) -> &mut WalletCore { + &mut self.wallet + } + + /// Get reference to the sequencer client. + #[must_use] + pub const fn sequencer_client(&self) -> &SequencerClient { + &self.sequencer_client + } + + /// Get reference to the indexer client. + #[must_use] + pub const fn indexer_client(&self) -> &IndexerClient { + &self.indexer_client + } + + /// Get existing public account IDs in the wallet. + #[must_use] + pub fn existing_public_accounts(&self) -> Vec { + self.wallet + .storage() + .user_data + .public_account_ids() + .collect() + } + + /// Get existing private account IDs in the wallet. + #[must_use] + pub fn existing_private_accounts(&self) -> Vec { + self.wallet + .storage() + .user_data + .private_account_ids() + .collect() + } + + pub fn get_last_block_sequencer(&self, runtime: &Arc) -> Result { + Ok(runtime.block_on(self.sequencer_client.get_last_block_id())?) + } + + pub fn get_last_block_indexer(&self, runtime: &Arc) -> Result { + Ok(runtime.block_on(self.indexer_client.get_last_finalized_block_id())?) + } +} + +impl Drop for TestContextFFI { + fn drop(&mut self) { + let Self { + sequencer_handle, + bedrock_compose, + _temp_indexer_dir: _, + _temp_sequencer_dir: _, + _temp_wallet_dir: _, + sequencer_client: _, + indexer_client: _, + wallet: _, + wallet_password: _, + } = self; + + let sequencer_handle = sequencer_handle + .take() + .expect("Sequencer handle should be present in TestContext drop"); + if !sequencer_handle.is_healthy() { + let Err(err) = sequencer_handle + .failed() + .now_or_never() + .expect("Sequencer handle should not be running"); + error!( + "Sequencer handle has unexpectedly stopped before TestContext drop with error: {err:#}" + ); + } + + let container = bedrock_compose + .service(BEDROCK_SERVICE_WITH_OPEN_PORT) + .unwrap_or_else(|| { + panic!("Failed to get Bedrock service container `{BEDROCK_SERVICE_WITH_OPEN_PORT}`") + }); + let output = std::process::Command::new("docker") + .args(["inspect", "-f", "{{.State.Running}}", container.id()]) + .output() + .expect("Failed to execute docker inspect command to check if Bedrock container is still running"); + let stdout = String::from_utf8(output.stdout) + .expect("Failed to parse docker inspect output as String"); + if stdout.trim() != "true" { + error!( + "Bedrock container `{}` is not running during TestContext drop, docker inspect output: {stdout}", + container.id() + ); + } + } +} + +/// A test context with ffi to be used in normal #[test] tests. +pub struct BlockingTestContextFFI { + ctx: Option, + runtime: Arc, + indexer_ffi: IndexerServiceFFI, +} + +impl BlockingTestContextFFI { + pub fn new() -> Result { + let runtime = tokio::runtime::Runtime::new().unwrap(); + let runtime_wrapped = Arc::new(runtime); + let (ctx, indexer_ffi) = TestContextFFI::new(&runtime_wrapped)?; + Ok(Self { + ctx: Some(ctx), + runtime: runtime_wrapped, + indexer_ffi, + }) + } + + #[must_use] + pub const fn ctx(&self) -> &TestContextFFI { + self.ctx.as_ref().expect("TestContext is set") + } + + #[must_use] + pub const fn ctx_mut(&mut self) -> &mut TestContextFFI { + self.ctx.as_mut().expect("TestContext is set") + } + + #[must_use] + pub const fn runtime(&self) -> &Arc { + &self.runtime + } + + #[must_use] + pub fn runtime_clone(&self) -> Arc { + Arc::::clone(&self.runtime) + } +} + +impl Drop for BlockingTestContextFFI { + fn drop(&mut self) { + let Self { + ctx, + runtime, + indexer_ffi, + } = self; + + // Ensure async cleanup of TestContext by blocking on its drop in the runtime. + runtime.block_on(async { + if let Some(ctx) = ctx.take() { + drop(ctx); + } + }); + + let indexer_handle = + // SAFETY: lib function ensures validity of value. + unsafe { indexer_ffi.handle() }; + + if !indexer_handle.is_healthy() { + error!("Indexer handle has unexpectedly stopped before TestContext drop"); + } + } +} 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 db3a4ead..d1e4f8ee 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, from_key_path: None, }; @@ -163,6 +164,7 @@ async fn amm_public() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 7, from_key_path: None, }; @@ -552,6 +554,7 @@ async fn amm_new_pool_using_labels() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 5, from_key_path: None, }; @@ -577,6 +580,7 @@ async fn amm_new_pool_using_labels() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 5, from_key_path: None, }; diff --git a/integration_tests/tests/ata.rs b/integration_tests/tests/ata.rs index ba487739..057b7817 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, from_key_path: None, }), @@ -501,6 +502,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, from_key_path: None, }), @@ -616,6 +618,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, from_key_path: None, }), diff --git a/integration_tests/tests/auth_transfer/private.rs b/integration_tests/tests/auth_transfer/private.rs index a7d74dec..58e5cfd3 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, to_key_path: None, from_key_path: None, @@ -73,6 +74,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, to_key_path: None, from_key_path: None, @@ -125,6 +127,7 @@ async fn deshielded_transfer_to_public_account() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 100, to_key_path: None, from_key_path: None, @@ -176,12 +179,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) @@ -192,6 +194,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, to_key_path: None, from_key_path: None, @@ -244,6 +247,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, to_key_path: None, from_key_path: None, @@ -290,6 +294,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, to_key_path: None, from_key_path: None, @@ -348,12 +353,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 @@ -364,6 +368,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, to_key_path: None, from_key_path: None, @@ -470,6 +475,7 @@ async fn private_transfer_using_from_label() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 100, from_key_path: None, to_key_path: None, @@ -545,3 +551,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 42ad6206..9eabbf5e 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, from_key_path: None, to_key_path: None, @@ -83,6 +84,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, from_key_path: None, to_key_path: None, @@ -123,6 +125,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, from_key_path: None, to_key_path: None, @@ -165,6 +168,7 @@ async fn two_consecutive_successful_transfers() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 100, from_key_path: None, to_key_path: None, @@ -201,6 +205,7 @@ async fn two_consecutive_successful_transfers() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 100, from_key_path: None, to_key_path: None, @@ -289,6 +294,7 @@ async fn successful_transfer_using_from_label() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 100, from_key_path: None, to_key_path: None, @@ -338,6 +344,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, from_key_path: None, to_key_path: None, diff --git a/integration_tests/tests/indexer.rs b/integration_tests/tests/indexer.rs index af9866f3..89bafe56 100644 --- a/integration_tests/tests/indexer.rs +++ b/integration_tests/tests/indexer.rs @@ -14,7 +14,6 @@ use integration_tests::{ }; use log::info; use nssa::AccountId; -use tokio::test; use wallet::cli::{Command, programs::native_token_transfer::AuthTransferSubcommand}; /// Maximum time to wait for the indexer to catch up to the sequencer. @@ -53,7 +52,7 @@ async fn wait_for_indexer_to_catch_up(ctx: &TestContext) -> u64 { }) } -#[test] +#[tokio::test] async fn indexer_test_run() -> Result<()> { let ctx = TestContext::new().await?; @@ -70,7 +69,7 @@ async fn indexer_test_run() -> Result<()> { Ok(()) } -#[test] +#[tokio::test] async fn indexer_block_batching() -> Result<()> { let ctx = TestContext::new().await?; @@ -101,7 +100,7 @@ async fn indexer_block_batching() -> Result<()> { Ok(()) } -#[test] +#[tokio::test] async fn indexer_state_consistency() -> Result<()> { let mut ctx = TestContext::new().await?; @@ -112,6 +111,7 @@ async fn indexer_state_consistency() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 100, from_key_path: None, to_key_path: None, @@ -150,6 +150,7 @@ async fn indexer_state_consistency() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 100, from_key_path: None, to_key_path: None, @@ -208,7 +209,7 @@ async fn indexer_state_consistency() -> Result<()> { Ok(()) } -#[test] +#[tokio::test] async fn indexer_state_consistency_with_labels() -> Result<()> { let mut ctx = TestContext::new().await?; @@ -238,6 +239,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, from_key_path: None, to_key_path: None, diff --git a/integration_tests/tests/indexer_ffi.rs b/integration_tests/tests/indexer_ffi.rs new file mode 100644 index 00000000..ac1dab4c --- /dev/null +++ b/integration_tests/tests/indexer_ffi.rs @@ -0,0 +1,287 @@ +#![expect( + clippy::shadow_unrelated, + clippy::tests_outside_test_module, + reason = "We don't care about these in tests" +)] + +use anyhow::{Context as _, Result}; +use indexer_service_rpc::RpcClient as _; +use integration_tests::{ + TIME_TO_WAIT_FOR_BLOCK_SECONDS, format_private_account_id, format_public_account_id, + test_context_ffi::BlockingTestContextFFI, verify_commitment_is_in_state, +}; +use log::info; +use nssa::AccountId; +use wallet::cli::{Command, programs::native_token_transfer::AuthTransferSubcommand}; + +/// Maximum time to wait for the indexer to catch up to the sequencer. +const L2_TO_L1_TIMEOUT_MILLIS: u64 = 180_000; + +#[test] +fn indexer_test_run_ffi() -> Result<()> { + let blocking_ctx = BlockingTestContextFFI::new()?; + let runtime_wrapped = blocking_ctx.runtime(); + + // RUN OBSERVATION + runtime_wrapped.block_on(async { + tokio::time::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)).await; + }); + + let last_block_indexer = blocking_ctx.ctx().get_last_block_indexer(runtime_wrapped)?; + + info!("Last block on ind now is {last_block_indexer}"); + + assert!(last_block_indexer > 1); + + Ok(()) +} + +#[test] +fn indexer_ffi_block_batching() -> Result<()> { + let blocking_ctx = BlockingTestContextFFI::new()?; + let runtime_wrapped = blocking_ctx.runtime(); + let ctx = blocking_ctx.ctx(); + + // WAIT + info!("Waiting for indexer to parse blocks"); + runtime_wrapped.block_on(async { + tokio::time::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)).await; + }); + + let last_block_indexer = runtime_wrapped + .block_on(ctx.indexer_client().get_last_finalized_block_id()) + .unwrap(); + + info!("Last block on ind now is {last_block_indexer}"); + + assert!(last_block_indexer > 1); + + // Getting wide batch to fit all blocks (from latest backwards) + let mut block_batch = runtime_wrapped + .block_on(ctx.indexer_client().get_blocks(None, 100)) + .unwrap(); + + // Reverse to check chain consistency from oldest to newest + block_batch.reverse(); + + // Checking chain consistency + let mut prev_block_hash = block_batch.first().unwrap().header.hash; + + for block in &block_batch[1..] { + assert_eq!(block.header.prev_block_hash, prev_block_hash); + + info!("Block {} chain-consistent", block.header.block_id); + + prev_block_hash = block.header.hash; + } + + Ok(()) +} + +#[test] +fn indexer_ffi_state_consistency() -> Result<()> { + let mut blocking_ctx = BlockingTestContextFFI::new()?; + let runtime_wrapped = blocking_ctx.runtime_clone(); + let ctx = blocking_ctx.ctx_mut(); + + let command = Command::AuthTransfer(AuthTransferSubcommand::Send { + from: Some(format_public_account_id(ctx.existing_public_accounts()[0])), + from_label: None, + to: Some(format_public_account_id(ctx.existing_public_accounts()[1])), + to_label: None, + to_npk: None, + to_vpk: None, + amount: 100, + to_identifier: Some(0), + }); + + runtime_wrapped.block_on(wallet::cli::execute_subcommand(ctx.wallet_mut(), command))?; + + info!("Waiting for next block creation"); + runtime_wrapped.block_on(async { + tokio::time::sleep(std::time::Duration::from_millis( + TIME_TO_WAIT_FOR_BLOCK_SECONDS, + )) + .await; + }); + + info!("Checking correct balance move"); + let acc_1_balance = + runtime_wrapped.block_on(sequencer_service_rpc::RpcClient::get_account_balance( + ctx.sequencer_client(), + ctx.existing_public_accounts()[0], + ))?; + let acc_2_balance = + runtime_wrapped.block_on(sequencer_service_rpc::RpcClient::get_account_balance( + ctx.sequencer_client(), + ctx.existing_public_accounts()[1], + ))?; + + info!("Balance of sender: {acc_1_balance:#?}"); + info!("Balance of receiver: {acc_2_balance:#?}"); + + assert_eq!(acc_1_balance, 9900); + assert_eq!(acc_2_balance, 20100); + + let from: AccountId = ctx.existing_private_accounts()[0]; + let to: AccountId = ctx.existing_private_accounts()[1]; + + let command = Command::AuthTransfer(AuthTransferSubcommand::Send { + from: Some(format_private_account_id(from)), + from_label: None, + to: Some(format_private_account_id(to)), + to_label: None, + to_npk: None, + to_vpk: None, + amount: 100, + to_identifier: Some(0), + }); + + runtime_wrapped.block_on(wallet::cli::execute_subcommand(ctx.wallet_mut(), command))?; + + info!("Waiting for next block creation"); + runtime_wrapped.block_on(async { + tokio::time::sleep(std::time::Duration::from_millis( + TIME_TO_WAIT_FOR_BLOCK_SECONDS, + )) + .await; + }); + + let new_commitment1 = ctx + .wallet() + .get_private_account_commitment(from) + .context("Failed to get private account commitment for sender")?; + let commitment_check1 = runtime_wrapped.block_on(verify_commitment_is_in_state( + new_commitment1, + ctx.sequencer_client(), + )); + assert!(commitment_check1); + + let new_commitment2 = ctx + .wallet() + .get_private_account_commitment(to) + .context("Failed to get private account commitment for receiver")?; + let commitment_check2 = runtime_wrapped.block_on(verify_commitment_is_in_state( + new_commitment2, + ctx.sequencer_client(), + )); + assert!(commitment_check2); + + info!("Successfully transferred privately to owned account"); + + // WAIT + info!("Waiting for indexer to parse blocks"); + runtime_wrapped.block_on(async { + tokio::time::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)).await; + }); + + let acc1_ind_state = runtime_wrapped.block_on( + ctx.indexer_client() + .get_account(ctx.existing_public_accounts()[0].into()), + )?; + let acc2_ind_state = runtime_wrapped.block_on( + ctx.indexer_client() + .get_account(ctx.existing_public_accounts()[1].into()), + )?; + + info!("Checking correct state transition"); + let acc1_seq_state = + runtime_wrapped.block_on(sequencer_service_rpc::RpcClient::get_account( + ctx.sequencer_client(), + ctx.existing_public_accounts()[0], + ))?; + let acc2_seq_state = + runtime_wrapped.block_on(sequencer_service_rpc::RpcClient::get_account( + ctx.sequencer_client(), + ctx.existing_public_accounts()[1], + ))?; + + assert_eq!(acc1_ind_state, acc1_seq_state.into()); + assert_eq!(acc2_ind_state, acc2_seq_state.into()); + + // ToDo: Check private state transition + + Ok(()) +} + +#[test] +fn indexer_ffi_state_consistency_with_labels() -> Result<()> { + let mut blocking_ctx = BlockingTestContextFFI::new()?; + let runtime_wrapped = blocking_ctx.runtime_clone(); + let ctx = blocking_ctx.ctx_mut(); + + // Assign labels to both accounts + let from_label = "idx-sender-label".to_owned(); + let to_label_str = "idx-receiver-label".to_owned(); + + let label_cmd = Command::Account(wallet::cli::account::AccountSubcommand::Label { + account_id: Some(format_public_account_id(ctx.existing_public_accounts()[0])), + account_label: None, + label: from_label.clone(), + }); + runtime_wrapped.block_on(wallet::cli::execute_subcommand(ctx.wallet_mut(), label_cmd))?; + + let label_cmd = Command::Account(wallet::cli::account::AccountSubcommand::Label { + account_id: Some(format_public_account_id(ctx.existing_public_accounts()[1])), + account_label: None, + label: to_label_str.clone(), + }); + runtime_wrapped.block_on(wallet::cli::execute_subcommand(ctx.wallet_mut(), label_cmd))?; + + // Send using labels instead of account IDs + let command = Command::AuthTransfer(AuthTransferSubcommand::Send { + from: None, + from_label: Some(from_label), + to: None, + to_label: Some(to_label_str), + to_npk: None, + to_vpk: None, + amount: 100, + to_identifier: Some(0), + }); + + runtime_wrapped.block_on(wallet::cli::execute_subcommand(ctx.wallet_mut(), command))?; + + info!("Waiting for next block creation"); + runtime_wrapped.block_on(async { + tokio::time::sleep(std::time::Duration::from_millis( + TIME_TO_WAIT_FOR_BLOCK_SECONDS, + )) + .await; + }); + + let acc_1_balance = + runtime_wrapped.block_on(sequencer_service_rpc::RpcClient::get_account_balance( + ctx.sequencer_client(), + ctx.existing_public_accounts()[0], + ))?; + let acc_2_balance = + runtime_wrapped.block_on(sequencer_service_rpc::RpcClient::get_account_balance( + ctx.sequencer_client(), + ctx.existing_public_accounts()[1], + ))?; + + assert_eq!(acc_1_balance, 9900); + assert_eq!(acc_2_balance, 20100); + + info!("Waiting for indexer to parse blocks"); + runtime_wrapped.block_on(async { + tokio::time::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)).await; + }); + + let acc1_ind_state = runtime_wrapped.block_on( + ctx.indexer_client() + .get_account(ctx.existing_public_accounts()[0].into()), + )?; + let acc1_seq_state = + runtime_wrapped.block_on(sequencer_service_rpc::RpcClient::get_account( + ctx.sequencer_client(), + ctx.existing_public_accounts()[0], + ))?; + + assert_eq!(acc1_ind_state, acc1_seq_state.into()); + + info!("Indexer state is consistent after label-based transfer"); + + Ok(()) +} diff --git a/integration_tests/tests/keys_restoration.rs b/integration_tests/tests/keys_restoration.rs index 38fa25bf..98099020 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, from_key_path: None, to_key_path: None, @@ -153,6 +153,7 @@ async fn restore_keys_from_seed() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 100, from_key_path: None, to_key_path: None, @@ -167,6 +168,7 @@ async fn restore_keys_from_seed() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 101, from_key_path: None, to_key_path: None, @@ -209,6 +211,7 @@ async fn restore_keys_from_seed() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 102, from_key_path: None, to_key_path: None, @@ -223,6 +226,7 @@ async fn restore_keys_from_seed() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 103, from_key_path: None, to_key_path: None, @@ -269,16 +273,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"); @@ -290,6 +294,7 @@ async fn restore_keys_from_seed() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 10, from_key_path: None, to_key_path: None, @@ -303,6 +308,7 @@ async fn restore_keys_from_seed() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 11, from_key_path: None, to_key_path: None, diff --git a/integration_tests/tests/token.rs b/integration_tests/tests/token.rs index 69e0d8e2..0dc3382a 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, from_key_path: None, }; @@ -229,6 +230,7 @@ async fn create_and_transfer_public_token() -> Result<()> { holder_label: None, holder_npk: None, holder_vpk: None, + holder_identifier: None, amount: mint_amount, }; @@ -374,6 +376,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, from_key_path: None, }; @@ -570,6 +573,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, }; @@ -618,6 +622,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, }; @@ -760,6 +765,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, from_key_path: None, }; @@ -892,6 +898,7 @@ async fn shielded_token_transfer() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: transfer_amount, from_key_path: None, }; @@ -1019,6 +1026,7 @@ async fn deshielded_token_transfer() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: transfer_amount, from_key_path: None, }; @@ -1138,12 +1146,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) @@ -1155,6 +1162,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, }; @@ -1358,6 +1366,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, from_key_path: None, }; diff --git a/integration_tests/tests/tps.rs b/integration_tests/tests/tps.rs index 7d2a6d29..41de30ed 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); @@ -249,7 +252,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { vec![sender_pre, recipient_pre], Program::serialize_instruction(balance_to_move).unwrap(), vec![1, 2], - vec![(sender_npk, sender_ss), (recipient_npk, recipient_ss)], + vec![(sender_npk, 0, sender_ss), (recipient_npk, 0, recipient_ss)], vec![sender_nsk], vec![Some(proof)], &program.into(), 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 215c7db8..c71003de 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, @@ -19,8 +19,8 @@ pub struct PrivacyPreservingCircuitInput { /// - `2` - private account without authentication /// - `3` - private PDA account pub visibility_mask: Vec, - /// Public keys of private accounts. - pub private_account_keys: Vec<(NullifierPublicKey, SharedSecretKey)>, + /// Public keys and identifiers of private accounts. + pub private_account_keys: Vec<(NullifierPublicKey, Identifier, SharedSecretKey)>, /// Nullifier secret keys for authorized private accounts. pub private_account_nsks: Vec, /// Membership proofs for private accounts. Can be [`None`] for uninitialized accounts. @@ -57,7 +57,7 @@ mod tests { use super::*; use crate::{ - Commitment, Nullifier, NullifierPublicKey, + Commitment, Nullifier, account::{Account, AccountId, AccountWithMetadata, Nonce}, }; @@ -94,12 +94,12 @@ mod tests { }], ciphertexts: vec![Ciphertext(vec![255, 255, 1, 1, 2, 2])], new_commitments: vec![Commitment::new( - &NullifierPublicKey::from(&[1; 32]), + &AccountId::new([1; 32]), &Account::default(), )], new_nullifiers: vec![( Nullifier::for_account_update( - &Commitment::new(&NullifierPublicKey::from(&[2; 32]), &Account::default()), + &Commitment::new(&AccountId::new([2; 32]), &Account::default()), &[1; 32], ), [0xab; 32], 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 a4fcdee1..478d475c 100644 --- a/nssa/core/src/lib.rs +++ b/nssa/core/src/lib.rs @@ -9,7 +9,7 @@ pub use commitment::{ compute_digest_for_path, }; pub use encryption::{EncryptionScheme, SharedSecretKey}; -pub use nullifier::{Nullifier, NullifierPublicKey, NullifierSecretKey}; +pub use nullifier::{Identifier, Nullifier, NullifierPublicKey, NullifierSecretKey}; pub mod account; mod circuit_io; 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 528bb372..f5bd8cea 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -2,8 +2,8 @@ use std::collections::{HashMap, VecDeque}; use borsh::{BorshDeserialize, BorshSerialize}; use nssa_core::{ - MembershipProof, NullifierPublicKey, NullifierSecretKey, PrivacyPreservingCircuitInput, - PrivacyPreservingCircuitOutput, SharedSecretKey, + Identifier, MembershipProof, NullifierPublicKey, NullifierSecretKey, + PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, SharedSecretKey, account::AccountWithMetadata, program::{ChainedCall, InstructionData, ProgramId, ProgramOutput}, }; @@ -68,7 +68,7 @@ pub fn execute_and_prove( pre_states: Vec, instruction_data: InstructionData, visibility_mask: Vec, - private_account_keys: Vec<(NullifierPublicKey, SharedSecretKey)>, + private_account_keys: Vec<(NullifierPublicKey, Identifier, SharedSecretKey)>, private_account_nsks: Vec, private_account_membership_proofs: Vec>, program_with_dependencies: &ProgramWithDependencies, @@ -213,11 +213,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; @@ -231,7 +228,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(), }; @@ -244,7 +241,7 @@ mod tests { vec![sender, recipient], Program::serialize_instruction(balance_to_move).unwrap(), vec![0, 2], - vec![(recipient_keys.npk(), shared_secret)], + vec![(recipient_keys.npk(), 0, shared_secret)], vec![], vec![None], &Program::authenticated_transfer_program().into(), @@ -261,7 +258,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], @@ -286,27 +283,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, ), ]; @@ -322,12 +316,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]; @@ -341,8 +335,8 @@ mod tests { Program::serialize_instruction(balance_to_move).unwrap(), vec![1, 2], vec![ - (sender_keys.npk(), shared_secret_1), - (recipient_keys.npk(), shared_secret_2), + (sender_keys.npk(), 0, shared_secret_1), + (recipient_keys.npk(), 0, shared_secret_2), ], vec![sender_keys.nsk], vec![commitment_set.get_proof_for(&commitment_sender), None], @@ -357,7 +351,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], @@ -366,7 +360,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], @@ -382,7 +376,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(); @@ -409,7 +403,7 @@ mod tests { vec![pre], instruction, vec![2], - vec![(account_keys.npk(), shared_secret)], + vec![(account_keys.npk(), 0, shared_secret)], vec![], vec![None], &program_with_deps, diff --git a/nssa/src/privacy_preserving_transaction/message.rs b/nssa/src/privacy_preserving_transaction/message.rs index 0fc30d4e..01e6e04f 100644 --- a/nssa/src/privacy_preserving_transaction/message.rs +++ b/nssa/src/privacy_preserving_transaction/message.rs @@ -167,9 +167,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], @@ -244,11 +246,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/privacy_preserving_transaction/witness_set.rs b/nssa/src/privacy_preserving_transaction/witness_set.rs index d7685a77..43a36671 100644 --- a/nssa/src/privacy_preserving_transaction/witness_set.rs +++ b/nssa/src/privacy_preserving_transaction/witness_set.rs @@ -14,7 +14,6 @@ pub struct WitnessSet { impl WitnessSet { #[must_use] // TODO: swap for Keycard signing path. - // However. we may need to get signatures from Keycard. pub fn for_message(message: &Message, proof: Proof, private_keys: &[&PrivateKey]) -> Self { let message_hash = message.hash_message(); let signatures_and_public_keys = private_keys diff --git a/nssa/src/public_transaction/witness_set.rs b/nssa/src/public_transaction/witness_set.rs index 8222c5ed..7a32c0ea 100644 --- a/nssa/src/public_transaction/witness_set.rs +++ b/nssa/src/public_transaction/witness_set.rs @@ -1,6 +1,6 @@ use borsh::{BorshDeserialize, BorshSerialize}; -use crate::{PrivateKey, PublicKey, Signature, public_transaction::Message}; +use crate::{PrivateKey, PublicKey, Signature, error::NssaError, public_transaction::Message}; #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct WitnessSet { @@ -8,19 +8,35 @@ pub struct WitnessSet { } impl WitnessSet { - #[must_use] - pub fn from_list(signatures: &[Signature], pub_keys: &[PublicKey]) -> Self { - assert_eq!(signatures.len(), pub_keys.len()); + pub fn from_list( + message: &Message, + signatures: &[Signature], + pub_keys: &[PublicKey], + ) -> Result { + if signatures.len() != pub_keys.len() { + return Err(NssaError::InvalidInput( + "`nssa::public_transaction::witness_set::from_list()`: mismatch in signature and public key counts".to_owned(), + )); + } + let message_hash = message.hash_message(); let signatures_and_public_keys = signatures .iter() .zip(pub_keys.iter()) - .map(|(sig, key)| (sig.clone(), key.clone())) - .collect(); + .map(|(sig, key)| { + if sig.is_valid_for(&message_hash, key) { + Ok((sig.clone(), key.clone())) + } else { + Err(NssaError::InvalidInput( + "`nssa::public_transaction::witness_set::from_list()`: signature does not correspond to public key".to_owned(), + )) + } + }) + .collect::>()?; - Self { + Ok(Self { signatures_and_public_keys, - } + }) } #[must_use] @@ -74,6 +90,73 @@ mod tests { use super::*; use crate::AccountId; + #[test] + fn from_list_accepts_valid_pairs() { + let key1 = PrivateKey::try_new([42; 32]).unwrap(); + let key2 = PrivateKey::try_new([13; 32]).unwrap(); + let pubkey1 = PublicKey::new_from_private_key(&key1); + let pubkey2 = PublicKey::new_from_private_key(&key2); + let addr1 = AccountId::from(&pubkey1); + let addr2 = AccountId::from(&pubkey2); + let message = Message::try_new::>( + [1_u32; 8], + vec![addr1, addr2], + vec![1_u128.into(), 2_u128.into()], + vec![], + ) + .unwrap(); + + let WitnessSet { + signatures_and_public_keys, + } = WitnessSet::for_message(&message, &[&key1, &key2]); + let (sigs, keys): (Vec<_>, Vec<_>) = signatures_and_public_keys.into_iter().unzip(); + + assert!(WitnessSet::from_list(&message, &sigs, &keys).is_ok()); + } + + #[test] + fn from_list_rejects_mismatched_pairs() { + let key1 = PrivateKey::try_new([42; 32]).unwrap(); + let key2 = PrivateKey::try_new([13; 32]).unwrap(); + let pubkey1 = PublicKey::new_from_private_key(&key1); + let pubkey2 = PublicKey::new_from_private_key(&key2); + let addr1 = AccountId::from(&pubkey1); + let addr2 = AccountId::from(&pubkey2); + let message = Message::try_new::>( + [1_u32; 8], + vec![addr1, addr2], + vec![1_u128.into(), 2_u128.into()], + vec![], + ) + .unwrap(); + + let WitnessSet { + signatures_and_public_keys, + } = WitnessSet::for_message(&message, &[&key1, &key2]); + let (sigs, keys): (Vec<_>, Vec<_>) = signatures_and_public_keys.into_iter().unzip(); + + // Swapped keys should be rejected. + assert!( + WitnessSet::from_list(&message, &sigs, &[keys[1].clone(), keys[0].clone()]).is_err() + ); + } + + #[test] + fn from_list_rejects_length_mismatch() { + let key1 = PrivateKey::try_new([1_u8; 32]).unwrap(); + let pubkey1 = PublicKey::new_from_private_key(&key1); + let addr1 = AccountId::from(&pubkey1); + let message = + Message::try_new::>([0; 8], vec![addr1], vec![1_u128.into()], vec![]).unwrap(); + + let WitnessSet { + signatures_and_public_keys, + } = WitnessSet::for_message(&message, &[&key1]); + let (sigs, _keys): (Vec<_>, Vec<_>) = signatures_and_public_keys.into_iter().unzip(); + + assert!(WitnessSet::from_list(&message, &sigs, &[]).is_err()); + } + #[test] fn for_message_constructor() { let key1 = PrivateKey::try_new([1; 32]).unwrap(); diff --git a/nssa/src/state.rs b/nssa/src/state.rs index f86f429f..ff16175c 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -459,7 +459,8 @@ pub mod tests { #[must_use] pub fn with_private_account(mut self, keys: &TestPrivateKeys, account: &Account) -> Self { - let commitment = Commitment::new(&keys.npk(), account); + let account_id = AccountId::from((&keys.npk(), 0)); + let commitment = Commitment::new(&account_id, account); self.private_state.0.extend(&[commitment]); self } @@ -617,13 +618,13 @@ pub mod tests { ..Account::default() }; - let npk1 = keys1.npk(); - let npk2 = keys2.npk(); + let account_id1 = AccountId::from((&keys1.npk(), 0)); + let account_id2 = AccountId::from((&keys2.npk(), 0)); - let init_commitment1 = Commitment::new(&npk1, &account); - let init_commitment2 = Commitment::new(&npk2, &account); - let init_nullifier1 = Nullifier::for_account_initialization(&npk1); - let init_nullifier2 = Nullifier::for_account_initialization(&npk2); + let init_commitment1 = Commitment::new(&account_id1, &account); + let init_commitment2 = Commitment::new(&account_id2, &account); + let init_nullifier1 = Nullifier::for_account_initialization(&account_id1); + let init_nullifier2 = Nullifier::for_account_initialization(&account_id2); let initial_private_accounts = vec![ (init_commitment1, init_nullifier1), @@ -1283,7 +1284,8 @@ pub mod tests { let sender_nonce = sender.account.nonce; - let recipient = AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk()); + let recipient = + AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); let esk = [3; 32]; let shared_secret = SharedSecretKey::new(&esk, &recipient_keys.vpk()); @@ -1293,7 +1295,7 @@ pub mod tests { vec![sender, recipient], Program::serialize_instruction(balance_to_move).unwrap(), vec![0, 2], - vec![(recipient_keys.npk(), shared_secret)], + vec![(recipient_keys.npk(), 0, shared_secret)], vec![], vec![None], &Program::authenticated_transfer_program().into(), @@ -1320,11 +1322,15 @@ pub mod tests { state: &V03State, ) -> PrivacyPreservingTransaction { let program = Program::authenticated_transfer_program(); - let sender_commitment = Commitment::new(&sender_keys.npk(), sender_private_account); - let sender_pre = - AccountWithMetadata::new(sender_private_account.clone(), true, &sender_keys.npk()); + let sender_account_id = AccountId::from((&sender_keys.npk(), 0)); + let sender_commitment = Commitment::new(&sender_account_id, sender_private_account); + let sender_pre = AccountWithMetadata::new( + sender_private_account.clone(), + true, + (&sender_keys.npk(), 0), + ); let recipient_pre = - AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk()); + AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); let esk_1 = [3; 32]; let shared_secret_1 = SharedSecretKey::new(&esk_1, &sender_keys.vpk()); @@ -1339,8 +1345,8 @@ pub mod tests { Program::serialize_instruction(balance_to_move).unwrap(), vec![1, 2], vec![ - (sender_keys.npk(), shared_secret_1), - (recipient_keys.npk(), shared_secret_2), + (sender_keys.npk(), 0, shared_secret_1), + (recipient_keys.npk(), 0, shared_secret_2), ], vec![sender_keys.nsk], vec![state.get_proof_for_commitment(&sender_commitment), None], @@ -1372,9 +1378,13 @@ pub mod tests { state: &V03State, ) -> PrivacyPreservingTransaction { let program = Program::authenticated_transfer_program(); - let sender_commitment = Commitment::new(&sender_keys.npk(), sender_private_account); - let sender_pre = - AccountWithMetadata::new(sender_private_account.clone(), true, &sender_keys.npk()); + let sender_account_id = AccountId::from((&sender_keys.npk(), 0)); + let sender_commitment = Commitment::new(&sender_account_id, sender_private_account); + let sender_pre = AccountWithMetadata::new( + sender_private_account.clone(), + true, + (&sender_keys.npk(), 0), + ); let recipient_pre = AccountWithMetadata::new( state.get_account_by_id(*recipient_account_id), false, @@ -1389,7 +1399,7 @@ pub mod tests { vec![sender_pre, recipient_pre], Program::serialize_instruction(balance_to_move).unwrap(), vec![1, 0], - vec![(sender_keys.npk(), shared_secret)], + vec![(sender_keys.npk(), 0, shared_secret)], vec![sender_keys.nsk], vec![state.get_proof_for_commitment(&sender_commitment)], &program.into(), @@ -1476,8 +1486,10 @@ pub mod tests { &state, ); + let sender_account_id = AccountId::from((&sender_keys.npk(), 0)); + let recipient_account_id = AccountId::from((&recipient_keys.npk(), 0)); let expected_new_commitment_1 = Commitment::new( - &sender_keys.npk(), + &sender_account_id, &Account { program_owner: Program::authenticated_transfer_program().id(), nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk), @@ -1486,15 +1498,15 @@ pub mod tests { }, ); - let sender_pre_commitment = Commitment::new(&sender_keys.npk(), &sender_private_account); + let sender_pre_commitment = Commitment::new(&sender_account_id, &sender_private_account); let expected_new_nullifier = Nullifier::for_account_update(&sender_pre_commitment, &sender_keys.nsk); let expected_new_commitment_2 = Commitment::new( - &recipient_keys.npk(), + &recipient_account_id, &Account { program_owner: Program::authenticated_transfer_program().id(), - nonce: Nonce::private_account_nonce_init(&recipient_keys.npk()), + nonce: Nonce::private_account_nonce_init(&recipient_account_id), balance: balance_to_move, ..Account::default() }, @@ -1553,8 +1565,9 @@ pub mod tests { &state, ); + let sender_account_id = AccountId::from((&sender_keys.npk(), 0)); let expected_new_commitment = Commitment::new( - &sender_keys.npk(), + &sender_account_id, &Account { program_owner: Program::authenticated_transfer_program().id(), nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk), @@ -1563,7 +1576,7 @@ pub mod tests { }, ); - let sender_pre_commitment = Commitment::new(&sender_keys.npk(), &sender_private_account); + let sender_pre_commitment = Commitment::new(&sender_account_id, &sender_private_account); let expected_new_nullifier = Nullifier::for_account_update(&sender_pre_commitment, &sender_keys.nsk); @@ -1895,10 +1908,10 @@ pub mod tests { ..Account::default() }, true, - &sender_keys.npk(), + (&sender_keys.npk(), 0), ); let private_account_2 = - AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk()); + AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); let result = execute_and_prove( vec![private_account_1, private_account_2], @@ -1907,10 +1920,12 @@ pub mod tests { vec![ ( sender_keys.npk(), + 0, SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), ), ( recipient_keys.npk(), + 0, SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), ), ], @@ -1933,7 +1948,7 @@ pub mod tests { ..Account::default() }, true, - &sender_keys.npk(), + (&sender_keys.npk(), 0), ); let private_account_2 = AccountWithMetadata::new(Account::default(), false, AccountId::new([1; 32])); @@ -1941,6 +1956,7 @@ pub mod tests { // Setting only one key for an execution with two private accounts. let private_account_keys = [( sender_keys.npk(), + 0, SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), )]; let result = execute_and_prove( @@ -1968,10 +1984,10 @@ pub mod tests { ..Account::default() }, true, - &sender_keys.npk(), + (&sender_keys.npk(), 0), ); let private_account_2 = - AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk()); + AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); // Setting no second commitment proof. let private_account_membership_proofs = [Some((0, vec![]))]; @@ -1982,10 +1998,12 @@ pub mod tests { vec![ ( sender_keys.npk(), + 0, SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), ), ( recipient_keys.npk(), + 0, SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), ), ], @@ -2009,10 +2027,10 @@ pub mod tests { ..Account::default() }, true, - &sender_keys.npk(), + (&sender_keys.npk(), 0), ); let private_account_2 = - AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk()); + AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); // Setting no auth key for an execution with one non default private accounts. let private_account_nsks = []; @@ -2023,10 +2041,12 @@ pub mod tests { vec![ ( sender_keys.npk(), + 0, SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), ), ( recipient_keys.npk(), + 0, SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), ), ], @@ -2050,20 +2070,22 @@ pub mod tests { ..Account::default() }, true, - &sender_keys.npk(), + (&sender_keys.npk(), 0), ); let private_account_2 = - AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk()); + AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); let private_account_keys = [ // First private account is the sender ( sender_keys.npk(), + 0, SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), ), // Second private account is the recipient ( recipient_keys.npk(), + 0, SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), ), ]; @@ -2098,7 +2120,7 @@ pub mod tests { ..Account::default() }, true, - &sender_keys.npk(), + (&sender_keys.npk(), 0), ); let private_account_2 = AccountWithMetadata::new( Account { @@ -2107,7 +2129,7 @@ pub mod tests { ..Account::default() }, false, - &recipient_keys.npk(), + (&recipient_keys.npk(), 0), ); let result = execute_and_prove( @@ -2117,10 +2139,12 @@ pub mod tests { vec![ ( sender_keys.npk(), + 0, SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), ), ( recipient_keys.npk(), + 0, SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), ), ], @@ -2144,7 +2168,7 @@ pub mod tests { ..Account::default() }, true, - &sender_keys.npk(), + (&sender_keys.npk(), 0), ); let private_account_2 = AccountWithMetadata::new( Account { @@ -2153,7 +2177,7 @@ pub mod tests { ..Account::default() }, false, - &recipient_keys.npk(), + (&recipient_keys.npk(), 0), ); let result = execute_and_prove( @@ -2163,10 +2187,12 @@ pub mod tests { vec![ ( sender_keys.npk(), + 0, SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), ), ( recipient_keys.npk(), + 0, SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), ), ], @@ -2190,7 +2216,7 @@ pub mod tests { ..Account::default() }, true, - &sender_keys.npk(), + (&sender_keys.npk(), 0), ); let private_account_2 = AccountWithMetadata::new( Account { @@ -2199,7 +2225,7 @@ pub mod tests { ..Account::default() }, false, - &recipient_keys.npk(), + (&recipient_keys.npk(), 0), ); let result = execute_and_prove( @@ -2209,10 +2235,12 @@ pub mod tests { vec![ ( sender_keys.npk(), + 0, SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), ), ( recipient_keys.npk(), + 0, SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), ), ], @@ -2236,7 +2264,7 @@ pub mod tests { ..Account::default() }, true, - &sender_keys.npk(), + (&sender_keys.npk(), 0), ); let private_account_2 = AccountWithMetadata::new( Account { @@ -2245,7 +2273,7 @@ pub mod tests { ..Account::default() }, false, - &recipient_keys.npk(), + (&recipient_keys.npk(), 0), ); let result = execute_and_prove( @@ -2255,10 +2283,12 @@ pub mod tests { vec![ ( sender_keys.npk(), + 0, SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), ), ( recipient_keys.npk(), + 0, SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), ), ], @@ -2283,13 +2313,13 @@ pub mod tests { ..Account::default() }, true, - &sender_keys.npk(), + (&sender_keys.npk(), 0), ); let private_account_2 = AccountWithMetadata::new( Account::default(), // This should be set to false in normal circumstances true, - &recipient_keys.npk(), + (&recipient_keys.npk(), 0), ); let result = execute_and_prove( @@ -2299,10 +2329,12 @@ pub mod tests { vec![ ( sender_keys.npk(), + 0, SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), ), ( recipient_keys.npk(), + 0, SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), ), ], @@ -2341,7 +2373,7 @@ pub mod tests { vec![public_account_1, private_pda_account], Program::serialize_instruction(10_u128).unwrap(), visibility_mask.to_vec(), - vec![(npk, shared_secret)], + vec![(npk, 0, shared_secret)], vec![], vec![None], &program.into(), @@ -2370,7 +2402,7 @@ pub mod tests { vec![pre_state], Program::serialize_instruction(seed).unwrap(), vec![3], - vec![(npk, shared_secret)], + vec![(npk, u128::MAX, shared_secret)], vec![], vec![None], &program.into(), @@ -2408,7 +2440,7 @@ pub mod tests { vec![pre_state], Program::serialize_instruction(seed).unwrap(), vec![3], - vec![(npk_b, shared_secret)], + vec![(npk_b, 0, shared_secret)], vec![], vec![None], &program.into(), @@ -2442,7 +2474,7 @@ pub mod tests { vec![pre_state], Program::serialize_instruction((seed, seed, callee_id)).unwrap(), vec![3], - vec![(npk, shared_secret)], + vec![(npk, u128::MAX, shared_secret)], vec![], vec![None], &program_with_deps, @@ -2479,7 +2511,7 @@ pub mod tests { vec![pre_state], Program::serialize_instruction((claim_seed, wrong_delegated_seed, callee_id)).unwrap(), vec![3], - vec![(npk, shared_secret)], + vec![(npk, 0, shared_secret)], vec![], vec![None], &program_with_deps, @@ -2515,7 +2547,7 @@ pub mod tests { vec![pre_a, pre_b], Program::serialize_instruction(seed).unwrap(), vec![3, 3], - vec![(keys_a.npk(), shared_a), (keys_b.npk(), shared_b)], + vec![(keys_a.npk(), 0, shared_a), (keys_b.npk(), 0, shared_b)], vec![], vec![None, None], &program.into(), @@ -2559,7 +2591,7 @@ pub mod tests { vec![owned_pre_state], Program::serialize_instruction(()).unwrap(), vec![3], - vec![(npk, shared_secret)], + vec![(npk, 0, shared_secret)], vec![], vec![None], &program.into(), @@ -2580,10 +2612,10 @@ pub mod tests { ..Account::default() }, true, - &sender_keys.npk(), + (&sender_keys.npk(), 0), ); let private_account_2 = - AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk()); + AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); let result = execute_and_prove( vec![private_account_1, private_account_2], @@ -2592,10 +2624,12 @@ pub mod tests { vec![ ( sender_keys.npk(), + 0, SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), ), ( recipient_keys.npk(), + 0, SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), ), ], @@ -2619,24 +2653,27 @@ pub mod tests { ..Account::default() }, true, - &sender_keys.npk(), + (&sender_keys.npk(), 0), ); let private_account_2 = - AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk()); + AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); // Setting three private account keys for a circuit execution with only two private // accounts. let private_account_keys = [ ( sender_keys.npk(), + 0, SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), ), ( recipient_keys.npk(), + 0, SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), ), ( sender_keys.npk(), + 0, SharedSecretKey::new(&[57; 32], &sender_keys.vpk()), ), ]; @@ -2665,10 +2702,10 @@ pub mod tests { ..Account::default() }, true, - &sender_keys.npk(), + (&sender_keys.npk(), 0), ); let private_account_2 = - AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk()); + AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); // Setting two private account keys for a circuit execution with only one non default // private account (visibility mask equal to 1 means that auth keys are expected). @@ -2682,10 +2719,12 @@ pub mod tests { vec![ ( sender_keys.npk(), + 0, SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), ), ( recipient_keys.npk(), + 0, SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), ), ], @@ -2764,7 +2803,7 @@ pub mod tests { ..Account::default() }, true, - &sender_keys.npk(), + (&sender_keys.npk(), 0), ); let visibility_mask = [1, 1]; @@ -2776,8 +2815,8 @@ pub mod tests { Program::serialize_instruction(100_u128).unwrap(), visibility_mask.to_vec(), vec![ - (sender_keys.npk(), shared_secret), - (sender_keys.npk(), shared_secret), + (sender_keys.npk(), 0, shared_secret), + (sender_keys.npk(), 0, shared_secret), ], private_account_nsks.to_vec(), private_account_membership_proofs.to_vec(), @@ -3091,14 +3130,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)); @@ -3112,7 +3153,7 @@ pub mod tests { vec![sender_pre, recipient_pre], Program::serialize_instruction(37_u128).unwrap(), vec![1, 0], - vec![(sender_keys.npk(), shared_secret)], + vec![(sender_keys.npk(), 0, shared_secret)], vec![sender_keys.nsk], vec![state.get_proof_for_commitment(&sender_commitment)], &program.into(), @@ -3164,7 +3205,7 @@ pub mod tests { ..Account::default() }, true, - &from_keys.npk(), + (&from_keys.npk(), 0), ); let to_account = AccountWithMetadata::new( Account { @@ -3172,13 +3213,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![ @@ -3217,21 +3260,21 @@ pub mod tests { nonce: from_new_nonce, ..from_account.account.clone() }; - let from_expected_commitment = Commitment::new(&from_keys.npk(), &from_expected_post); + let from_expected_commitment = Commitment::new(&from_account_id, &from_expected_post); let to_expected_post = Account { balance: u128::from(number_of_calls) * amount, nonce: to_new_nonce, ..to_account.account.clone() }; - let to_expected_commitment = Commitment::new(&to_keys.npk(), &to_expected_post); + let to_expected_commitment = Commitment::new(&to_account_id, &to_expected_post); // Act let (output, proof) = execute_and_prove( vec![to_account, from_account], Program::serialize_instruction(instruction).unwrap(), vec![1, 1], - vec![(from_keys.npk(), to_ss), (to_keys.npk(), from_ss)], + vec![(from_keys.npk(), 0, to_ss), (to_keys.npk(), 0, from_ss)], vec![from_keys.nsk, to_keys.nsk], vec![ state.get_proof_for_commitment(&from_commitment), @@ -3483,7 +3526,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(); @@ -3500,7 +3543,7 @@ pub mod tests { vec![authorized_account], Program::serialize_instruction(balance).unwrap(), vec![1], - vec![(private_keys.npk(), shared_secret)], + vec![(private_keys.npk(), 0, shared_secret)], vec![private_keys.nsk], vec![None], &program.into(), @@ -3522,7 +3565,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)); } @@ -3536,7 +3580,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]; @@ -3547,7 +3591,7 @@ pub mod tests { vec![unauthorized_account], Program::serialize_instruction(0_u128).unwrap(), vec![2], - vec![(private_keys.npk(), shared_secret)], + vec![(private_keys.npk(), 0, shared_secret)], vec![], vec![None], &program.into(), @@ -3569,7 +3613,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)); } @@ -3582,7 +3627,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(); @@ -3598,7 +3643,7 @@ pub mod tests { vec![authorized_account.clone()], Program::serialize_instruction(balance).unwrap(), vec![1], - vec![(private_keys.npk(), shared_secret)], + vec![(private_keys.npk(), 0, shared_secret)], vec![private_keys.nsk], vec![None], &claimer_program.into(), @@ -3624,7 +3669,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 @@ -3643,7 +3689,7 @@ pub mod tests { vec![account_metadata], Program::serialize_instruction(()).unwrap(), vec![1], - vec![(private_keys.npk(), shared_secret2)], + vec![(private_keys.npk(), 0, shared_secret2)], vec![private_keys.nsk], vec![None], &noop_program.into(), @@ -3711,7 +3757,7 @@ pub mod tests { let program = Program::changer_claimer(); let sender_keys = test_private_account_keys_1(); let private_account = - AccountWithMetadata::new(Account::default(), true, &sender_keys.npk()); + AccountWithMetadata::new(Account::default(), true, (&sender_keys.npk(), 0)); // Don't change data (None) and don't claim (false) let instruction: (Option>, bool) = (None, false); @@ -3721,6 +3767,7 @@ pub mod tests { vec![1], vec![( sender_keys.npk(), + 0, SharedSecretKey::new(&[3; 32], &sender_keys.vpk()), )], vec![sender_keys.nsk], @@ -3737,7 +3784,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); @@ -3748,6 +3795,7 @@ pub mod tests { vec![1], vec![( sender_keys.npk(), + 0, SharedSecretKey::new(&[3; 32], &sender_keys.vpk()), )], vec![sender_keys.nsk], @@ -3777,11 +3825,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)], @@ -3804,7 +3853,7 @@ pub mod tests { vec![sender_account, recipient_account], Program::serialize_instruction(instruction).unwrap(), vec![0, 1], - vec![(recipient_keys.npk(), recipient)], + vec![(recipient_keys.npk(), 0, recipient)], vec![recipient_keys.nsk], vec![state.get_proof_for_commitment(&recipient_commitment)], &program_with_deps, @@ -3939,7 +3988,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]; @@ -3954,7 +4003,7 @@ pub mod tests { vec![pre], Program::serialize_instruction(instruction).unwrap(), vec![2], - vec![(account_keys.npk(), shared_secret)], + vec![(account_keys.npk(), 0, shared_secret)], vec![], vec![None], &validity_window_program.into(), @@ -4008,7 +4057,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]; @@ -4023,7 +4072,7 @@ pub mod tests { vec![pre], Program::serialize_instruction(instruction).unwrap(), vec![2], - vec![(account_keys.npk(), shared_secret)], + vec![(account_keys.npk(), 0, shared_secret)], vec![], vec![None], &validity_window_program.into(), diff --git a/program_methods/guest/src/bin/privacy_preserving_circuit.rs b/program_methods/guest/src/bin/privacy_preserving_circuit.rs index 8018cd80..70979b7e 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, - PrivacyPreservingCircuitOutput, SharedSecretKey, + Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, Identifier, + MembershipProof, Nullifier, NullifierPublicKey, NullifierSecretKey, + PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, SharedSecretKey, account::{Account, AccountId, AccountWithMetadata, Nonce}, compute_digest_for_path, program::{ @@ -17,6 +17,8 @@ use nssa_core::{ }; use risc0_zkvm::{guest::env, serde::to_vec}; +const PRIVATE_PDA_FIXED_IDENTIFIER: u128 = u128::MAX; + /// State of the involved accounts before and after program execution. struct ExecutionState { pre_states: Vec, @@ -54,7 +56,7 @@ impl ExecutionState { /// Validate program outputs and derive the overall execution state. pub fn derive_from_outputs( visibility_mask: &[u8], - private_account_keys: &[(NullifierPublicKey, SharedSecretKey)], + private_account_keys: &[(NullifierPublicKey, Identifier, SharedSecretKey)], program_id: ProgramId, program_outputs: Vec, ) -> Self { @@ -67,7 +69,7 @@ impl ExecutionState { let mut keys_iter = private_account_keys.iter(); for (pos, &mask) in visibility_mask.iter().enumerate() { if matches!(mask, 1..=3) { - let (npk, _) = keys_iter.next().unwrap_or_else(|| { + let (npk, _, _) = keys_iter.next().unwrap_or_else(|| { panic!( "private_account_keys shorter than visibility_mask demands: no key for masked position {pos} (mask {mask})" ) @@ -487,7 +489,7 @@ fn resolve_authorization_and_record_bindings( fn compute_circuit_output( execution_state: ExecutionState, visibility_mask: &[u8], - private_account_keys: &[(NullifierPublicKey, SharedSecretKey)], + private_account_keys: &[(NullifierPublicKey, Identifier, SharedSecretKey)], private_account_nsks: &[NullifierSecretKey], private_account_membership_proofs: &[Option], ) -> PrivacyPreservingCircuitOutput { @@ -523,16 +525,18 @@ fn compute_circuit_output( output.public_post_states.push(post_state); } 1 | 2 => { - let Some((npk, shared_secret)) = private_keys_iter.next() else { + let Some((npk, identifier, shared_secret)) = private_keys_iter.next() else { panic!("Missing private account key"); }; - - assert_eq!( - AccountId::from(npk), - pre_state.account_id, - "AccountId mismatch" + assert_ne!( + *identifier, PRIVATE_PDA_FIXED_IDENTIFIER, + "Identifier must be different from {PRIVATE_PDA_FIXED_IDENTIFIER}. This is reserved for private PDA." ); + let account_id = AccountId::from((npk, *identifier)); + + assert_eq!(account_id, pre_state.account_id, "AccountId mismatch"); + let (new_nullifier, new_nonce) = if account_visibility_mask == 1 { // Private account with authentication @@ -560,7 +564,7 @@ fn compute_circuit_output( let new_nullifier = compute_nullifier_and_set_digest( membership_proof_opt.as_ref(), &pre_state.account, - npk, + &account_id, nsk, ); @@ -590,9 +594,9 @@ fn compute_circuit_output( "Membership proof must be None for unauthorized accounts" ); - let nullifier = Nullifier::for_account_initialization(npk); + let nullifier = Nullifier::for_account_initialization(&account_id); - let new_nonce = Nonce::private_account_nonce_init(npk); + let new_nonce = Nonce::private_account_nonce_init(&account_id); ((nullifier, DUMMY_COMMITMENT_HASH), new_nonce) }; @@ -603,11 +607,12 @@ fn compute_circuit_output( post_with_updated_nonce.nonce = new_nonce; // Compute commitment - let commitment_post = Commitment::new(npk, &post_with_updated_nonce); + let commitment_post = Commitment::new(&account_id, &post_with_updated_nonce); // Encrypt and push post state let encrypted_account = EncryptionScheme::encrypt( &post_with_updated_nonce, + *identifier, shared_secret, &commitment_post, output_index, @@ -628,10 +633,15 @@ fn compute_circuit_output( // `private_pda_bound_positions` check) guarantees that every mask-3 // position has been through at least one such binding, so this // branch can safely use the wallet npk without re-verifying. - let Some((npk, shared_secret)) = private_keys_iter.next() else { + let Some((npk, identifier, shared_secret)) = private_keys_iter.next() else { panic!("Missing private account key"); }; + assert_eq!( + *identifier, PRIVATE_PDA_FIXED_IDENTIFIER, + "Identifier for private PDAs must be {PRIVATE_PDA_FIXED_IDENTIFIER}." + ); + let (new_nullifier, new_nonce) = if pre_state.is_authorized { // Existing private PDA with authentication (like mask 1) let Some(nsk) = private_nsks_iter.next() else { @@ -650,7 +660,7 @@ fn compute_circuit_output( let new_nullifier = compute_nullifier_and_set_digest( membership_proof_opt.as_ref(), &pre_state.account, - npk, + &pre_state.account_id, nsk, ); let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk); @@ -677,8 +687,8 @@ fn compute_circuit_output( "Membership proof must be None for new accounts" ); - let nullifier = Nullifier::for_account_initialization(npk); - let new_nonce = Nonce::private_account_nonce_init(npk); + let nullifier = Nullifier::for_account_initialization(&pre_state.account_id); + let new_nonce = Nonce::private_account_nonce_init(&pre_state.account_id); ((nullifier, DUMMY_COMMITMENT_HASH), new_nonce) }; output.new_nullifiers.push(new_nullifier); @@ -686,10 +696,12 @@ fn compute_circuit_output( let mut post_with_updated_nonce = post_state; post_with_updated_nonce.nonce = new_nonce; - let commitment_post = Commitment::new(npk, &post_with_updated_nonce); + let commitment_post = + Commitment::new(&pre_state.account_id, &post_with_updated_nonce); let encrypted_account = EncryptionScheme::encrypt( &post_with_updated_nonce, + PRIVATE_PDA_FIXED_IDENTIFIER, shared_secret, &commitment_post, output_index, @@ -726,7 +738,7 @@ fn compute_circuit_output( fn compute_nullifier_and_set_digest( membership_proof_opt: Option<&MembershipProof>, pre_account: &Account, - npk: &NullifierPublicKey, + account_id: &AccountId, nsk: &NullifierSecretKey, ) -> (Nullifier, CommitmentSetDigest) { membership_proof_opt.as_ref().map_or_else( @@ -738,12 +750,12 @@ fn compute_nullifier_and_set_digest( ); // Compute initialization nullifier - let nullifier = Nullifier::for_account_initialization(npk); + let nullifier = Nullifier::for_account_initialization(account_id); (nullifier, DUMMY_COMMITMENT_HASH) }, |membership_proof| { // Compute commitment set digest associated with provided auth path - let commitment_pre = Commitment::new(npk, pre_account); + let commitment_pre = Commitment::new(account_id, pre_account); let set_digest = compute_digest_for_path(&commitment_pre, membership_proof); // Compute update nullifier diff --git a/sequencer/core/src/lib.rs b/sequencer/core/src/lib.rs index 47037fbd..22c09d85 100644 --- a/sequencer/core/src/lib.rs +++ b/sequencer/core/src/lib.rs @@ -110,6 +110,7 @@ impl SequencerCore SequencerCore u64 { #[expect(clippy::shadow_unrelated, reason = "Fine for tests")] #[cfg(test)] mod tests { + use common::test_utils::produce_dummy_block; use nssa::{AccountId, PublicKey}; use tempfile::tempdir; @@ -302,7 +336,7 @@ mod tests { let transfer_tx = common::test_utils::create_transaction_native_token_transfer(from, 0, to, 1, &sign_key); - let block = common::test_utils::produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]); + let block = produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]); dbio.put_block(&block, [1; 32]).unwrap(); @@ -369,11 +403,7 @@ mod tests { 1, &sign_key, ); - let block = common::test_utils::produce_dummy_block( - (i + 1).into(), - Some(prev_hash), - vec![transfer_tx], - ); + let block = produce_dummy_block((i + 1).into(), Some(prev_hash), vec![transfer_tx]); dbio.put_block(&block, [i; 32]).unwrap(); } @@ -439,7 +469,7 @@ mod tests { let prev_hash = last_block.header.hash; let transfer_tx = common::test_utils::create_transaction_native_token_transfer(from, 0, to, 1, &sign_key); - let block = common::test_utils::produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]); + let block = produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]); let control_hash1 = block.header.hash; @@ -451,7 +481,7 @@ mod tests { let prev_hash = last_block.header.hash; let transfer_tx = common::test_utils::create_transaction_native_token_transfer(from, 1, to, 1, &sign_key); - let block = common::test_utils::produce_dummy_block(3, Some(prev_hash), vec![transfer_tx]); + let block = produce_dummy_block(3, Some(prev_hash), vec![transfer_tx]); let control_hash2 = block.header.hash; @@ -466,7 +496,7 @@ mod tests { let control_tx_hash1 = transfer_tx.hash(); - let block = common::test_utils::produce_dummy_block(4, Some(prev_hash), vec![transfer_tx]); + let block = produce_dummy_block(4, Some(prev_hash), vec![transfer_tx]); dbio.put_block(&block, [3; 32]).unwrap(); let last_id = dbio.get_meta_last_block_in_db().unwrap(); @@ -478,7 +508,7 @@ mod tests { let control_tx_hash2 = transfer_tx.hash(); - let block = common::test_utils::produce_dummy_block(5, Some(prev_hash), vec![transfer_tx]); + let block = produce_dummy_block(5, Some(prev_hash), vec![transfer_tx]); dbio.put_block(&block, [4; 32]).unwrap(); let control_block_id1 = dbio.get_block_id_by_hash(control_hash1.0).unwrap().unwrap(); @@ -526,7 +556,7 @@ mod tests { let prev_hash = last_block.header.hash; let transfer_tx = common::test_utils::create_transaction_native_token_transfer(from, 0, to, 1, &sign_key); - let block = common::test_utils::produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]); + let block = produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]); block_res.push(block.clone()); dbio.put_block(&block, [1; 32]).unwrap(); @@ -537,7 +567,7 @@ mod tests { let prev_hash = last_block.header.hash; let transfer_tx = common::test_utils::create_transaction_native_token_transfer(from, 1, to, 1, &sign_key); - let block = common::test_utils::produce_dummy_block(3, Some(prev_hash), vec![transfer_tx]); + let block = produce_dummy_block(3, Some(prev_hash), vec![transfer_tx]); block_res.push(block.clone()); dbio.put_block(&block, [2; 32]).unwrap(); @@ -549,7 +579,7 @@ mod tests { let transfer_tx = common::test_utils::create_transaction_native_token_transfer(from, 2, to, 1, &sign_key); - let block = common::test_utils::produce_dummy_block(4, Some(prev_hash), vec![transfer_tx]); + let block = produce_dummy_block(4, Some(prev_hash), vec![transfer_tx]); block_res.push(block.clone()); dbio.put_block(&block, [3; 32]).unwrap(); @@ -560,7 +590,7 @@ mod tests { let transfer_tx = common::test_utils::create_transaction_native_token_transfer(from, 3, to, 1, &sign_key); - let block = common::test_utils::produce_dummy_block(5, Some(prev_hash), vec![transfer_tx]); + let block = produce_dummy_block(5, Some(prev_hash), vec![transfer_tx]); block_res.push(block.clone()); dbio.put_block(&block, [4; 32]).unwrap(); @@ -633,11 +663,7 @@ mod tests { tx_hash_res.push(transfer_tx1.hash().0); tx_hash_res.push(transfer_tx2.hash().0); - let block = common::test_utils::produce_dummy_block( - 2, - Some(prev_hash), - vec![transfer_tx1, transfer_tx2], - ); + let block = produce_dummy_block(2, Some(prev_hash), vec![transfer_tx1, transfer_tx2]); dbio.put_block(&block, [1; 32]).unwrap(); @@ -652,11 +678,7 @@ mod tests { tx_hash_res.push(transfer_tx1.hash().0); tx_hash_res.push(transfer_tx2.hash().0); - let block = common::test_utils::produce_dummy_block( - 3, - Some(prev_hash), - vec![transfer_tx1, transfer_tx2], - ); + let block = produce_dummy_block(3, Some(prev_hash), vec![transfer_tx1, transfer_tx2]); dbio.put_block(&block, [2; 32]).unwrap(); @@ -671,11 +693,7 @@ mod tests { tx_hash_res.push(transfer_tx1.hash().0); tx_hash_res.push(transfer_tx2.hash().0); - let block = common::test_utils::produce_dummy_block( - 4, - Some(prev_hash), - vec![transfer_tx1, transfer_tx2], - ); + let block = produce_dummy_block(4, Some(prev_hash), vec![transfer_tx1, transfer_tx2]); dbio.put_block(&block, [3; 32]).unwrap(); @@ -687,7 +705,7 @@ mod tests { common::test_utils::create_transaction_native_token_transfer(from, 6, to, 1, &sign_key); tx_hash_res.push(transfer_tx.hash().0); - let block = common::test_utils::produce_dummy_block(5, Some(prev_hash), vec![transfer_tx]); + let block = produce_dummy_block(5, Some(prev_hash), vec![transfer_tx]); dbio.put_block(&block, [4; 32]).unwrap(); diff --git a/testnet_initial_state/src/lib.rs b/testnet_initial_state/src/lib.rs index 07d546fe..f6f1e288 100644 --- a/testnet_initial_state/src/lib.rs +++ b/testnet_initial_state/src/lib.rs @@ -95,9 +95,16 @@ pub struct PublicAccountPrivateInitialData { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PrivateAccountPrivateInitialData { - pub account_id: nssa::AccountId, pub account: nssa_core::account::Account, pub key_chain: KeyChain, + pub identifier: nssa_core::Identifier, +} + +impl PrivateAccountPrivateInitialData { + #[must_use] + pub fn account_id(&self) -> nssa::AccountId { + nssa::AccountId::from((&self.key_chain.nullifier_public_key, self.identifier)) + } } #[must_use] @@ -142,7 +149,6 @@ pub fn initial_priv_accounts_private_keys() -> Vec 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 d7dc9841..982df0f3 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 a3634367..3bfdb383 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; @@ -72,19 +72,32 @@ impl WalletChainStore { public_tree.insert( data.account_id, data.chain_index, - data.data.expect("Expect valid public account keys"), + data.data.expect("`chain_storage::WalletChainStore::new()`: failed to produce a Key Tree for a PersistentAccountData."), ); } 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)], + }, + ); } }, } @@ -117,13 +130,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)], + }, + ); } } } @@ -176,28 +196,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; + } + } } } } @@ -205,7 +268,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::*; @@ -234,7 +297,7 @@ mod tests { data: Some(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 bc63e0bb..5ed99d90 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -79,7 +79,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. @@ -88,6 +89,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 { @@ -146,6 +154,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 @@ -153,14 +170,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!( @@ -172,6 +183,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) + } } } } @@ -231,7 +265,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) @@ -274,21 +308,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 8508f0f6..101b8d1c 100644 --- a/wallet/src/cli/programs/native_token_transfer.rs +++ b/wallet/src/cli/programs/native_token_transfer.rs @@ -58,6 +58,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, @@ -140,6 +144,7 @@ impl WalletSubcommand for AuthTransferSubcommand { to_label, to_npk, to_vpk, + to_identifier, amount, from_key_path, to_key_path, @@ -237,6 +242,7 @@ impl WalletSubcommand for AuthTransferSubcommand { from, to_npk, to_vpk, + to_identifier, amount, }, ) @@ -247,6 +253,7 @@ impl WalletSubcommand for AuthTransferSubcommand { from, to_npk, to_vpk, + to_identifier, amount, }, ) @@ -335,6 +342,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, @@ -372,6 +382,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, @@ -413,6 +426,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandPrivate { from, to_npk, to_vpk, + to_identifier, amount, } => { let from: AccountId = from.parse().unwrap(); @@ -428,7 +442,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}"); @@ -487,6 +507,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded { from, to_npk, to_vpk, + to_identifier, amount, } => { let from: AccountId = from.parse().unwrap(); @@ -503,7 +524,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 83582c87..e22881bc 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, @@ -230,6 +238,7 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { to_label, to_npk, to_vpk, + to_identifier, amount, from_key_path, } => { @@ -317,6 +326,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, }, ), @@ -325,6 +335,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, }, ), @@ -409,6 +420,7 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { holder_label, holder_npk, holder_vpk, + holder_identifier, amount, } => { let definition = resolve_id_or_label( @@ -497,6 +509,7 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { definition_account_id: definition, holder_npk, holder_vpk, + holder_identifier, amount, }, ), @@ -505,6 +518,7 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { definition_account_id: definition, holder_npk, holder_vpk, + holder_identifier, amount, }, ), @@ -592,6 +606,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, }, @@ -621,6 +638,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, }, @@ -680,6 +700,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, }, @@ -709,6 +732,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, }, @@ -869,6 +895,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(); @@ -889,6 +916,7 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate { sender_account_id, recipient_npk, recipient_vpk, + recipient_identifier.unwrap_or_else(rand::random), balance_to_move, ) .await?; @@ -986,6 +1014,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(); @@ -1007,6 +1036,7 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate { definition_account_id, holder_npk, holder_vpk, + holder_identifier.unwrap_or_else(rand::random), amount, ) .await?; @@ -1151,6 +1181,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(); @@ -1171,6 +1202,7 @@ impl WalletSubcommand for TokenProgramSubcommandShielded { sender_account_id, recipient_npk, recipient_vpk, + recipient_identifier.unwrap_or_else(rand::random), balance_to_move, ) .await?; @@ -1290,6 +1322,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(); @@ -1311,6 +1344,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 cdedee1b..185648e4 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 b4420b02..54883fdb 100644 --- a/wallet/src/helperfunctions.rs +++ b/wallet/src/helperfunctions.rs @@ -186,17 +186,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 { @@ -209,15 +208,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 59c7781d..0de34efe 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, @@ -259,13 +259,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. @@ -298,13 +320,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. @@ -337,7 +360,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, @@ -350,7 +373,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 => {} } @@ -396,7 +419,7 @@ impl WalletCore { acc_manager.visibility_mask().to_vec(), private_account_keys .iter() - .map(|keys| (keys.npk, keys.ssk)) + .map(|keys| (keys.npk, keys.identifier, keys.ssk)) .collect::>(), acc_manager.private_account_auth(), acc_manager.private_account_membership_proofs(), @@ -416,7 +439,7 @@ impl WalletCore { ) .unwrap(); - let witness_set = Self::sign_privacy_message(&message, &proof, &acc_manager); + let witness_set = Self::sign_privacy_message(&message, proof.clone(), &acc_manager); let tx = PrivacyPreservingTransaction::new(message, witness_set); let shared_secrets: Vec<_> = private_account_keys @@ -481,33 +504,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); @@ -519,18 +542,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); } } @@ -574,12 +603,12 @@ impl WalletCore { #[must_use] pub fn sign_privacy_message( message: &nssa::privacy_preserving_transaction::Message, - proof: &Proof, + proof: Proof, acc_manager: &privacy_preserving_tx::AccountManager, ) -> nssa::privacy_preserving_transaction::witness_set::WitnessSet { nssa::privacy_preserving_transaction::witness_set::WitnessSet::for_message( message, - proof.clone(), + proof, &acc_manager.public_account_auth(), ) } diff --git a/wallet/src/privacy_preserving_tx.rs b/wallet/src/privacy_preserving_tx.rs index 14a805c7..3df2ecc1 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, SharedSecretKey, + Identifier, MembershipProof, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, account::{AccountWithMetadata, Nonce}, encryption::{EphemeralPublicKey, ViewingPublicKey}, }; @@ -16,6 +16,7 @@ pub enum PrivacyPreservingAccount { PrivateForeign { npk: NullifierPublicKey, vpk: ViewingPublicKey, + identifier: Identifier, }, } @@ -29,13 +30,19 @@ impl PrivacyPreservingAccount { pub const fn is_private(&self) -> bool { matches!( &self, - Self::PrivateOwned(_) | Self::PrivateForeign { npk: _, vpk: _ } + Self::PrivateOwned(_) + | Self::PrivateForeign { + npk: _, + vpk: _, + identifier: _ + } ) } } pub struct PrivateAccountKeys { pub npk: NullifierPublicKey, + pub identifier: Identifier, pub ssk: SharedSecretKey, pub vpk: ViewingPublicKey, pub epk: EphemeralPublicKey, @@ -81,12 +88,17 @@ impl AccountManager { (State::Private(pre), mask) } - PrivacyPreservingAccount::PrivateForeign { npk, vpk } => { + PrivacyPreservingAccount::PrivateForeign { + npk, + vpk, + identifier, + } => { let acc = nssa_core::account::Account::default(); - let auth_acc = AccountWithMetadata::new(acc, false, &npk); + let auth_acc = AccountWithMetadata::new(acc, false, (&npk, identifier)); let pre = AccountPreparedData { nsk: None, npk, + identifier, vpk, pre_state: auth_acc, proof: None, @@ -139,6 +151,7 @@ impl AccountManager { Some(PrivateAccountKeys { npk: pre.npk, + identifier: pre.identifier, ssk: eph_holder.calculate_shared_secret_sender(&pre.vpk), vpk: pre.vpk.clone(), epk: eph_holder.generate_ephemeral_public_key(), @@ -193,6 +206,7 @@ impl AccountManager { struct AccountPreparedData { nsk: Option, npk: NullifierPublicKey, + identifier: Identifier, vpk: ViewingPublicKey, pre_state: AccountWithMetadata, proof: Option, @@ -202,11 +216,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); }; @@ -224,11 +235,12 @@ async fn private_acc_preparation( // TODO: Technically we could allow unauthorized owned accounts, but currently we don't have // support from that in the wallet. - let sender_pre = AccountWithMetadata::new(from_acc.clone(), true, &from_npk); + let sender_pre = AccountWithMetadata::new(from_acc.clone(), true, (&from_npk, from_identifier)); Ok(AccountPreparedData { nsk: Some(nsk), npk: from_npk, + identifier: from_identifier, vpk: from_vpk, pre_state: sender_pre, proof, 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/public.rs b/wallet/src/program_facades/native_token_transfer/public.rs index b48d8a7d..1a7c7f36 100644 --- a/wallet/src/program_facades/native_token_transfer/public.rs +++ b/wallet/src/program_facades/native_token_transfer/public.rs @@ -62,8 +62,9 @@ impl NativeTokenTransfer<'_> { WitnessSet::from_list(&[from_sig], &[from_pk]) } } else { - let sign_ids = self.0.filter_owned_accounts(&[from, to]); - WalletCore::sign_public_message(self.0, &message, &sign_ids)? + // Silently skips accounts without signing keys + let witness_set = WalletCore::sign_public_message(self.0, &message, &sign_ids) + .expect("`WalletCore::sign_public_message()` failed to produce a signature for a NativeTokenTransfer."); }; let tx = PublicTransaction::new(message, witness_set); 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,