diff --git a/Cargo.lock b/Cargo.lock index 85286be6..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", @@ -1959,7 +1959,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 2.0.117", ] [[package]] @@ -2108,7 +2108,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2409,7 +2409,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -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", @@ -5415,7 +5426,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -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", @@ -7037,6 +7048,17 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" +[[package]] +name = "rpassword" +version = "7.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2501c67132bd19c3005b0111fba298907ef002c8c1cf68e25634707e38bf66fe" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.61.2", +] + [[package]] name = "rpds" version = "1.2.0" @@ -7069,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", @@ -7111,6 +7133,16 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "rtoolbox" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50a0e551c1e27e1731aba276dbeaeac73f53c7cd34d1bda485d02bd1e0f36844" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "ruint" version = "1.17.2" @@ -7120,7 +7152,7 @@ dependencies = [ "borsh", "proptest", "rand 0.8.5", - "rand 0.9.2", + "rand 0.9.3", "ruint-macro", "serde_core", "valuable", @@ -7164,7 +7196,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -8103,7 +8135,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -8818,7 +8850,7 @@ dependencies = [ "http", "httparse", "log", - "rand 0.9.2", + "rand 0.9.3", "sha1", "thiserror 2.0.18", "utf-8", @@ -9117,6 +9149,7 @@ dependencies = [ "optfield", "pyo3", "rand 0.8.5", + "rpassword", "sequencer_service_rpc", "serde", "serde_json", @@ -9126,6 +9159,7 @@ dependencies = [ "token_core", "tokio", "url", + "zeroize", ] [[package]] @@ -9394,7 +9428,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 149df056..551c1f98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ members = [ "bedrock_client", "testnet_initial_state", "keycard_wallet", + "indexer_ffi", ] [workspace.dependencies] @@ -56,6 +57,7 @@ indexer_core = { path = "indexer/core" } indexer_service = { path = "indexer/service" } indexer_service_protocol = { path = "indexer/service/protocol" } indexer_service_rpc = { path = "indexer/service/rpc" } +indexer_ffi = { path = "indexer_ffi" } wallet = { path = "wallet" } wallet-ffi = { path = "wallet-ffi", default-features = false } clock_core = { path = "programs/clock/core" } 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/associated_token_account.bin b/artifacts/program_methods/associated_token_account.bin index be94122a..5439d1af 100644 Binary files a/artifacts/program_methods/associated_token_account.bin and b/artifacts/program_methods/associated_token_account.bin differ diff --git a/artifacts/program_methods/authenticated_transfer.bin b/artifacts/program_methods/authenticated_transfer.bin index 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 3949a495..d1e4f8ee 100644 --- a/integration_tests/tests/amm.rs +++ b/integration_tests/tests/amm.rs @@ -133,8 +133,8 @@ async fn amm_public() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 7, - pin: None, from_key_path: None, }; @@ -164,8 +164,8 @@ async fn amm_public() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 7, - pin: None, from_key_path: None, }; @@ -554,8 +554,8 @@ async fn amm_new_pool_using_labels() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 5, - pin: None, from_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -580,8 +580,8 @@ async fn amm_new_pool_using_labels() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 5, - pin: None, from_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; diff --git a/integration_tests/tests/ata.rs b/integration_tests/tests/ata.rs index cbde7021..057b7817 100644 --- a/integration_tests/tests/ata.rs +++ b/integration_tests/tests/ata.rs @@ -268,8 +268,8 @@ async fn transfer_and_burn_via_ata() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: fund_amount, - pin: None, from_key_path: None, }), ) @@ -502,8 +502,8 @@ async fn transfer_via_ata_private_owner() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: fund_amount, - pin: None, from_key_path: None, }), ) @@ -618,8 +618,8 @@ async fn burn_via_ata_private_owner() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: fund_amount, - pin: None, from_key_path: None, }), ) diff --git a/integration_tests/tests/auth_transfer/private.rs b/integration_tests/tests/auth_transfer/private.rs index 9e8dce2a..f08e3759 100644 --- a/integration_tests/tests/auth_transfer/private.rs +++ b/integration_tests/tests/auth_transfer/private.rs @@ -30,8 +30,8 @@ async fn private_transfer_to_owned_account() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 100, - pin: None, to_key_path: None, from_key_path: None, }); @@ -74,8 +74,8 @@ 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, - pin: None, to_key_path: None, from_key_path: None, }); @@ -127,8 +127,8 @@ async fn deshielded_transfer_to_public_account() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 100, - pin: None, to_key_path: None, from_key_path: None, }); @@ -179,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) @@ -195,8 +194,8 @@ 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, - pin: None, to_key_path: None, from_key_path: None, }); @@ -248,8 +247,8 @@ async fn shielded_transfer_to_owned_private_account() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 100, - pin: None, to_key_path: None, from_key_path: None, }); @@ -295,8 +294,8 @@ 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, - pin: None, to_key_path: None, from_key_path: None, }); @@ -354,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 @@ -370,8 +368,8 @@ 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, - pin: None, to_key_path: None, from_key_path: None, }); @@ -420,7 +418,6 @@ async fn initialize_private_account() -> Result<()> { let command = Command::AuthTransfer(AuthTransferSubcommand::Init { account_id: Some(format_private_account_id(account_id)), account_label: None, - pin: None, key_path: None, }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -478,8 +475,8 @@ async fn private_transfer_using_from_label() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 100, - pin: None, from_key_path: None, to_key_path: None, }); @@ -525,7 +522,6 @@ async fn initialize_private_account_using_label() -> Result<()> { let command = Command::AuthTransfer(AuthTransferSubcommand::Init { account_id: None, account_label: Some(label), - pin: None, key_path: None, }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -555,3 +551,116 @@ 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, + from_key_path: None, + to_key_path: None, + }), + ) + .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, + from_key_path: None, + to_key_path: None, + }), + ) + .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 5be41c09..9eabbf5e 100644 --- a/integration_tests/tests/auth_transfer/public.rs +++ b/integration_tests/tests/auth_transfer/public.rs @@ -23,8 +23,8 @@ async fn successful_transfer_to_existing_account() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 100, - pin: None, from_key_path: None, to_key_path: None, }); @@ -84,8 +84,8 @@ pub async fn successful_transfer_to_new_account() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 100, - pin: None, from_key_path: None, to_key_path: None, }); @@ -125,8 +125,8 @@ 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, - pin: None, from_key_path: None, to_key_path: None, }); @@ -168,8 +168,8 @@ async fn two_consecutive_successful_transfers() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 100, - pin: None, from_key_path: None, to_key_path: None, }); @@ -205,8 +205,8 @@ async fn two_consecutive_successful_transfers() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 100, - pin: None, from_key_path: None, to_key_path: None, }); @@ -253,7 +253,6 @@ async fn initialize_public_account() -> Result<()> { let command = Command::AuthTransfer(AuthTransferSubcommand::Init { account_id: Some(format_public_account_id(account_id)), account_label: None, - pin: None, key_path: None, }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -295,8 +294,8 @@ async fn successful_transfer_using_from_label() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 100, - pin: None, from_key_path: None, to_key_path: None, }); @@ -345,8 +344,8 @@ async fn successful_transfer_using_to_label() -> Result<()> { to_label: Some(label), to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 100, - pin: None, from_key_path: None, to_key_path: None, }); diff --git a/integration_tests/tests/indexer.rs b/integration_tests/tests/indexer.rs index 5d0c5b0d..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,8 +111,8 @@ async fn indexer_state_consistency() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 100, - pin: None, from_key_path: None, to_key_path: None, }); @@ -151,8 +150,8 @@ async fn indexer_state_consistency() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 100, - pin: None, from_key_path: None, to_key_path: None, }); @@ -210,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?; @@ -240,8 +239,8 @@ 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, - pin: None, 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..2730f9b5 --- /dev/null +++ b/integration_tests/tests/indexer_ffi.rs @@ -0,0 +1,293 @@ +#![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), + from_key_path: None, + to_key_path: None, + }); + + 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), + from_key_path: None, + to_key_path: None, + }); + + 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), + from_key_path: None, + to_key_path: None, + }); + + 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 7b165a00..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,8 +74,8 @@ 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, - pin: None, from_key_path: None, to_key_path: None, }); @@ -154,8 +153,8 @@ async fn restore_keys_from_seed() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 100, - pin: None, from_key_path: None, to_key_path: None, }); @@ -169,8 +168,8 @@ async fn restore_keys_from_seed() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 101, - pin: None, from_key_path: None, to_key_path: None, }); @@ -212,8 +211,8 @@ async fn restore_keys_from_seed() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 102, - pin: None, from_key_path: None, to_key_path: None, }); @@ -227,8 +226,8 @@ async fn restore_keys_from_seed() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 103, - pin: None, from_key_path: None, to_key_path: None, }); @@ -274,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"); @@ -295,8 +294,8 @@ async fn restore_keys_from_seed() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 10, - pin: None, from_key_path: None, to_key_path: None, }); @@ -309,8 +308,8 @@ async fn restore_keys_from_seed() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: 11, - pin: None, from_key_path: None, to_key_path: None, }); diff --git a/integration_tests/tests/pinata.rs b/integration_tests/tests/pinata.rs index 82873843..fcc77a76 100644 --- a/integration_tests/tests/pinata.rs +++ b/integration_tests/tests/pinata.rs @@ -54,7 +54,6 @@ async fn claim_pinata_to_uninitialized_public_account_fails_fast() -> Result<()> Command::Pinata(PinataProgramAgnosticSubcommand::Claim { to: Some(winner_account_id_formatted), to_label: None, - pin: None, key_path: None, }), ) @@ -111,7 +110,6 @@ async fn claim_pinata_to_uninitialized_private_account_fails_fast() -> Result<() Command::Pinata(PinataProgramAgnosticSubcommand::Claim { to: Some(winner_account_id_formatted), to_label: None, - pin: None, key_path: None, }), ) @@ -145,7 +143,6 @@ async fn claim_pinata_to_existing_public_account() -> Result<()> { let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim { to: Some(format_public_account_id(ctx.existing_public_accounts()[0])), to_label: None, - pin: None, key_path: None, }); @@ -188,7 +185,6 @@ async fn claim_pinata_to_existing_private_account() -> Result<()> { ctx.existing_private_accounts()[0], )), to_label: None, - pin: None, key_path: None, }); @@ -255,7 +251,6 @@ async fn claim_pinata_to_new_private_account() -> Result<()> { let command = Command::AuthTransfer(AuthTransferSubcommand::Init { account_id: Some(winner_account_id_formatted.clone()), account_label: None, - pin: None, key_path: None, }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -273,7 +268,6 @@ async fn claim_pinata_to_new_private_account() -> Result<()> { let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim { to: Some(winner_account_id_formatted), to_label: None, - pin: None, key_path: None, }); diff --git a/integration_tests/tests/token.rs b/integration_tests/tests/token.rs index f226b52e..0dc3382a 100644 --- a/integration_tests/tests/token.rs +++ b/integration_tests/tests/token.rs @@ -134,8 +134,8 @@ async fn create_and_transfer_public_token() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: transfer_amount, - pin: None, from_key_path: None, }; @@ -182,7 +182,6 @@ async fn create_and_transfer_public_token() -> Result<()> { holder: Some(format_public_account_id(recipient_account_id)), holder_label: None, amount: burn_amount, - holder_pin: None, holder_key_path: None, }; @@ -231,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, }; @@ -376,8 +376,8 @@ 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, - pin: None, from_key_path: None, }; @@ -406,7 +406,6 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> { holder: Some(format_private_account_id(recipient_account_id)), holder_label: None, amount: burn_amount, - holder_pin: None, holder_key_path: None, }; @@ -574,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, }; @@ -622,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, }; @@ -764,8 +765,8 @@ 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, - pin: None, from_key_path: None, }; @@ -897,8 +898,8 @@ async fn shielded_token_transfer() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: transfer_amount, - pin: None, from_key_path: None, }; @@ -1025,8 +1026,8 @@ async fn deshielded_token_transfer() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: transfer_amount, - pin: None, from_key_path: None, }; @@ -1145,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) @@ -1162,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, }; @@ -1365,8 +1366,8 @@ async fn transfer_token_using_from_label() -> Result<()> { to_label: None, to_npk: None, to_vpk: None, + to_identifier: Some(0), amount: transfer_amount, - pin: None, from_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; diff --git a/integration_tests/tests/tps.rs b/integration_tests/tests/tps.rs index 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/keycard_wallet/src/lib.rs b/keycard_wallet/src/lib.rs index c968a682..37b010fd 100644 --- a/keycard_wallet/src/lib.rs +++ b/keycard_wallet/src/lib.rs @@ -46,21 +46,24 @@ impl KeycardWallet { .call_method1("get_public_key_for_path", (path,))? .extract()?; - let public_key: [u8; 32] = public_key.try_into().expect("Expect 32 bytes"); + let public_key: [u8; 32] = public_key.try_into().map_err(|vec: Vec| { + PyErr::new::(format!( + "expected 32-byte public key from keycard, got {} bytes", + vec.len() + )) + })?; - Ok(PublicKey::try_new(public_key).expect("Expect a valid public key1")) + PublicKey::try_new(public_key) + .map_err(|e| PyErr::new::(e.to_string())) } - #[must_use] - pub fn get_public_key_for_path_with_connect(pin: &str, path: &str) -> PublicKey { - let pub_key = Python::with_gil(|py| { - python_path::add_python_path(py).expect("keycard_wallet.py not found"); + pub fn get_public_key_for_path_with_connect(pin: &str, path: &str) -> PyResult { + Python::with_gil(|py| { + python_path::add_python_path(py)?; - let wallet = Self::new(py).expect("Expect keycard wallet"); + let wallet = Self::new(py)?; - let is_connected = wallet - .setup_communication(py, pin) - .expect("Expect a Boolean."); + let is_connected = wallet.setup_communication(py, pin)?; if is_connected { log::info!("\u{2705} Keycard is now connected to wallet."); @@ -72,8 +75,7 @@ impl KeycardWallet { drop(wallet.disconnect(py)); pub_key - }); - pub_key.expect("Expect a valid public key2") + }) } pub fn sign_message_for_path( @@ -81,7 +83,7 @@ impl KeycardWallet { py: Python, path: &str, message: &[u8; 32], - ) -> PyResult { + ) -> PyResult<(Signature, PublicKey)> { let py_signature: Vec = self .instance .bind(py) @@ -96,22 +98,27 @@ impl KeycardWallet { )) })?; - Ok(Signature { value: signature }) + let sig = Signature { value: signature }; + let pub_key = self.get_public_key_for_path(py, path)?; + if !sig.is_valid_for(message, &pub_key) { + return Err(PyErr::new::( + "keycard returned a signature that does not verify against its own public key", + )); + } + Ok((sig, pub_key)) } pub fn sign_message_for_path_with_connect( pin: &str, path: &str, message: &[u8; 32], - ) -> PyResult { + ) -> PyResult<(Signature, PublicKey)> { Python::with_gil(|py| { - python_path::add_python_path(py).expect("keycard_wallet.py not found"); + python_path::add_python_path(py)?; - let wallet = Self::new(py).expect("Expect keycard wallet"); + let wallet = Self::new(py)?; - let is_connected = wallet - .setup_communication(py, pin) - .expect("Expect a Boolean."); + let is_connected = wallet.setup_communication(py, pin)?; if is_connected { log::info!("\u{2705} Keycard is now connected to wallet."); @@ -119,11 +126,11 @@ impl KeycardWallet { log::info!("\u{274c} Keycard is not connected to wallet."); } - let signature = wallet.sign_message_for_path(py, path, message); + let result = wallet.sign_message_for_path(py, path, message); drop(wallet.disconnect(py)); - signature + result }) } @@ -134,10 +141,9 @@ impl KeycardWallet { Ok(()) } - #[must_use] - pub fn get_account_id_for_path_with_connect(pin: &str, key_path: &str) -> String { - let public_key = Self::get_public_key_for_path_with_connect(pin, key_path); + pub fn get_account_id_for_path_with_connect(pin: &str, key_path: &str) -> PyResult { + let public_key = Self::get_public_key_for_path_with_connect(pin, key_path)?; - format!("Public/{}", AccountId::from(&public_key)) + Ok(format!("Public/{}", AccountId::from(&public_key))) } } diff --git a/keycard_wallet/src/python_path.rs b/keycard_wallet/src/python_path.rs index b98061eb..b7159295 100644 --- a/keycard_wallet/src/python_path.rs +++ b/keycard_wallet/src/python_path.rs @@ -6,11 +6,32 @@ use pyo3::{prelude::*, types::PyList}; pub fn add_python_path(py: Python<'_>) -> PyResult<()> { let current_dir = env::current_dir().expect("Failed to get current working directory"); - let paths_to_add: Vec = vec![ - current_dir.join("python"), - current_dir.join("python").join("keycard-py"), + let python_base = env::var("VIRTUAL_ENV") + .ok() + .and_then(|v| PathBuf::from(v).parent().map(PathBuf::from)) + .unwrap_or_else(|| current_dir.clone()); + + let mut paths_to_add: Vec = vec![ + python_base.join("python"), + python_base.join("python").join("keycard-py"), ]; + // If a virtualenv is active, add its site-packages so that dependencies + // installed in the venv (e.g. smartcard, ecdsa) are importable by the + // pyo3 embedded interpreter, which does not inherit sys.path from the + // shell's `python3` executable. + if let Ok(venv) = env::var("VIRTUAL_ENV") { + let lib = PathBuf::from(&venv).join("lib"); + if let Ok(entries) = std::fs::read_dir(&lib) { + for entry in entries.flatten() { + let site_packages = entry.path().join("site-packages"); + if site_packages.exists() { + paths_to_add.push(site_packages); + } + } + } + } + // Sanity check — warns early if a path doesn't exist for path in &paths_to_add { if !path.exists() { 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/python/keycard_wallet.py b/python/keycard_wallet.py index 73c27fa9..7d7e5bd3 100644 --- a/python/keycard_wallet.py +++ b/python/keycard_wallet.py @@ -9,17 +9,11 @@ from keycard import constants import keycard -PIN = '123456' -PUK = '123456123456' DEFAULT_PAIRING_PASSWORD = "KeycardDefaultPairing" -DEFAULT_MNEMONIC = "fashion degree mountain wool question damp current pond grow dolphin chronic then" -DEFAULT_PASSPHRASE = "" class KeycardWallet: def __init__(self): self.card = KeyCard() - self.pairing_index = None - self.pairing_key = None def _is_smart_card_reader_detected(self) -> bool: try: @@ -35,7 +29,6 @@ class KeycardWallet: # No readers, no card, or card doesn't respond. return False - # Wrapped def is_unpaired_keycard_available(self) -> bool: if not self._is_smart_card_reader_detected(): return False @@ -43,20 +36,16 @@ class KeycardWallet: return False return True - # Wrapped - def setup_communication(self, pin = PIN, password = DEFAULT_PAIRING_PASSWORD) -> bool: + def setup_communication(self, pin: str, password = DEFAULT_PAIRING_PASSWORD) -> bool: try: self.card.select() if not self.card.is_initialized: return False - if self.pairing_index is None: - pairing_index, pairing_key = self.card.pair(password) - self.pairing_index = pairing_index - self.pairing_key = pairing_key + pairing_index, pairing_key = self.card.pair(password) + self.pairing_index = pairing_index - self.card.open_secure_channel(pairing_index, pairing_key) self.card.verify_pin(pin) @@ -65,11 +54,11 @@ class KeycardWallet: print(f"Error: {e}") return False - def load_mnemonic(self, mnemonic = DEFAULT_MNEMONIC, passphrase = DEFAULT_PASSPHRASE) -> bool: + def load_mnemonic(self, mnemonic: str) -> bool: try: # Convert mnemonic to seed mnemo = Mnemonic("english") - seed = mnemo.to_seed(mnemonic, passphrase) + seed = mnemo.to_seed(mnemonic) # Load the LEE seed onto the card result = self.card.load_key( @@ -77,6 +66,7 @@ class KeycardWallet: lee_seed = seed ) + #TODO: this appears to be the issue. return True except Exception as e: print(f"Error during disconnect: {e}") @@ -88,8 +78,6 @@ class KeycardWallet: return None self.card.unpair(self.pairing_index) - self.pairing_index = None - self.pairing_key = None return True except Exception as e: @@ -118,7 +106,7 @@ class KeycardWallet: return None - def sign_message_for_path(self, message: bytes = b"DefaultMessageTestDefaultMessage", path: str = "m/44'/60'/0'/0/0") -> bytes | None: + def sign_message_for_path(self, message: bytes, path: str = "m/44'/60'/0'/0/0") -> bytes | None: try: if not self.card.is_secure_channel_open or not self.card.is_pin_verified: return None 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 d4c6573d..88b8c963 100644 --- a/wallet-ffi/src/transfer.rs +++ b/wallet-ffi/src/transfer.rs @@ -12,7 +12,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, }; @@ -83,7 +83,7 @@ pub unsafe extern "C" fn wallet_ffi_transfer_public( let transfer = NativeTokenTransfer(&wallet); - match block_on(transfer.send_public_transfer(from_id, to_id, amount, &None, &None)) { + match block_on(transfer.send_public_transfer(from_id, to_id, amount, None, None)) { Ok(tx_hash) => { let tx_hash = CString::new(tx_hash.to_string()) .map_or(ptr::null_mut(), std::ffi::CString::into_raw); @@ -113,6 +113,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 /// @@ -136,6 +137,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], pin: *const c_char, key_path: *const c_char, @@ -146,7 +148,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; } @@ -168,6 +175,7 @@ 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 pin = optional_c_str(pin); let key_path = optional_c_str(key_path); @@ -292,6 +300,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 /// @@ -315,6 +324,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], pin: *const c_char, key_path: *const c_char, @@ -325,7 +335,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; } @@ -347,6 +362,7 @@ 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 pin = optional_c_str(pin); let key_path = optional_c_str(key_path); @@ -604,7 +620,7 @@ pub unsafe extern "C" fn wallet_ffi_register_public_account( let transfer = NativeTokenTransfer(&wallet); - match block_on(transfer.register_account(account_id, &None, &None)) { + match block_on(transfer.register_account(account_id, None)) { Ok(tx_hash) => { 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 f5c84720..1f6075f6 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], const char *pin, const char *key_path, @@ -757,6 +792,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 * @@ -779,6 +815,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], const char *pin, const char *key_path, diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index 4d8750a4..a4d5cf0a 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -19,6 +19,8 @@ testnet_initial_state.workspace = true ata_core.workspace = true bip39.workspace = true pyo3.workspace = true +rpassword = "7" +zeroize = "1" anyhow.workspace = true thiserror.workspace = true 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 9bdd7132..5ed99d90 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -28,19 +28,12 @@ pub enum AccountSubcommand { #[arg(short, long)] keys: bool, /// Valid 32 byte base58 string with privacy prefix. - #[arg(short, long, conflicts_with = "account_label", required_unless_present_any = ["account_label", "pin"])] + #[arg(short, long, conflicts_with = "account_label", required_unless_present_any = ["account_label", "key_path"])] account_id: Option, /// Account label (alternative to --account-id). #[arg(long, conflicts_with = "account_id")] account_label: Option, - #[arg( - long, - conflicts_with = "account_id", - conflicts_with = "account_label", - requires = "key_path" - )] - pin: Option, - #[arg(long)] + #[arg(long, conflicts_with = "account_id", conflicts_with = "account_label")] key_path: Option, }, /// Produce new public or private account. @@ -86,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. @@ -95,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 { @@ -153,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 @@ -160,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!( @@ -179,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) + } } } } @@ -195,7 +222,6 @@ impl WalletSubcommand for AccountSubcommand { keys, account_id, account_label, - pin, key_path, } => { let resolved = resolve_id_or_label( @@ -203,8 +229,7 @@ impl WalletSubcommand for AccountSubcommand { account_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &pin, - &key_path, + key_path.as_deref(), )?; let (account_id_str, addr_kind) = parse_addr_with_privacy_prefix(&resolved)?; @@ -240,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) @@ -283,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 } => { @@ -418,8 +429,7 @@ impl WalletSubcommand for AccountSubcommand { account_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, + None, )?; let (account_id_str, _) = parse_addr_with_privacy_prefix(&resolved)?; diff --git a/wallet/src/cli/keycard.rs b/wallet/src/cli/keycard.rs index 8dde98e4..3c35ebda 100644 --- a/wallet/src/cli/keycard.rs +++ b/wallet/src/cli/keycard.rs @@ -5,7 +5,7 @@ use pyo3::prelude::*; use crate::{ WalletCore, - cli::{SubcommandReturnValue, WalletSubcommand}, + cli::{SubcommandReturnValue, WalletSubcommand, read_pin}, }; /// Represents generic chain CLI subcommand. @@ -15,8 +15,6 @@ pub enum KeycardSubcommand { Load { #[arg(short, long)] mnemonic: Option, - #[arg(short, long)] - pin: Option, }, } @@ -45,7 +43,9 @@ impl WalletSubcommand for KeycardSubcommand { Ok(SubcommandReturnValue::Empty) } - Self::Load { mnemonic, pin } => { + Self::Load { mnemonic } => { + let pin = read_pin()?; + Python::with_gil(|py| { python_path::add_python_path(py).expect("keycard_wallet.py not found"); @@ -53,10 +53,7 @@ impl WalletSubcommand for KeycardSubcommand { .expect("`wallet::keycard::load`: invalid keycard wallet provided"); let is_connected = wallet - .setup_communication( - py, - &pin.expect("`wallet::keycard::load`: invalid data received for pin"), - ) + .setup_communication(py, &pin) .expect("Expect a Boolean."); if is_connected { diff --git a/wallet/src/cli/mod.rs b/wallet/src/cli/mod.rs index 1948db24..1ce0ea2c 100644 --- a/wallet/src/cli/mod.rs +++ b/wallet/src/cli/mod.rs @@ -8,6 +8,7 @@ use futures::TryFutureExt as _; use nssa::{ProgramDeploymentTransaction, program::Program}; use sequencer_service_rpc::RpcClient as _; +pub use crate::helperfunctions::read_pin; use crate::{ WalletCore, cli::{ diff --git a/wallet/src/cli/programs/amm.rs b/wallet/src/cli/programs/amm.rs index aa0c5482..1805f444 100644 --- a/wallet/src/cli/programs/amm.rs +++ b/wallet/src/cli/programs/amm.rs @@ -216,24 +216,21 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand { user_holding_a_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, + None, )?; let user_holding_b = resolve_id_or_label( user_holding_b, user_holding_b_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, + None, )?; let user_holding_lp = resolve_id_or_label( user_holding_lp, user_holding_lp_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, + None, )?; let (user_holding_a, user_holding_a_privacy) = parse_addr_with_privacy_prefix(&user_holding_a)?; @@ -288,16 +285,14 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand { user_holding_a_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, + None, )?; let user_holding_b = resolve_id_or_label( user_holding_b, user_holding_b_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, + None, )?; let (user_holding_a, user_holding_a_privacy) = parse_addr_with_privacy_prefix(&user_holding_a)?; @@ -378,24 +373,21 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand { user_holding_a_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, + None, )?; let user_holding_b = resolve_id_or_label( user_holding_b, user_holding_b_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, + None, )?; let user_holding_lp = resolve_id_or_label( user_holding_lp, user_holding_lp_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, + None, )?; let (user_holding_a, user_holding_a_privacy) = parse_addr_with_privacy_prefix(&user_holding_a)?; @@ -453,24 +445,21 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand { user_holding_a_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, + None, )?; let user_holding_b = resolve_id_or_label( user_holding_b, user_holding_b_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, + None, )?; let user_holding_lp = resolve_id_or_label( user_holding_lp, user_holding_lp_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, + None, )?; let (user_holding_a, user_holding_a_privacy) = parse_addr_with_privacy_prefix(&user_holding_a)?; diff --git a/wallet/src/cli/programs/native_token_transfer.rs b/wallet/src/cli/programs/native_token_transfer.rs index f0497d18..5dcf684c 100644 --- a/wallet/src/cli/programs/native_token_transfer.rs +++ b/wallet/src/cli/programs/native_token_transfer.rs @@ -1,7 +1,6 @@ use anyhow::Result; use clap::Subcommand; use common::transaction::NSSATransaction; -use keycard_wallet::KeycardWallet; use nssa::AccountId; use crate::{ @@ -24,14 +23,13 @@ pub enum AuthTransferSubcommand { #[arg( long, conflicts_with = "account_label", - // required_unless_present = "account_label" + required_unless_present_any = ["account_label", "key_path"] )] account_id: Option, /// Account label (alternative to --account-id). #[arg(long, conflicts_with = "account_id")] account_label: Option, - #[arg(long)] - pin: Option, + /// Key path (alternative to --account-id) is used to retrieve data from Keycard. #[arg(long, conflicts_with = "account_id", conflicts_with = "account_label")] key_path: Option, }, @@ -43,7 +41,7 @@ pub enum AuthTransferSubcommand { /// First is used for owned accounts, second otherwise. Send { /// from - valid 32 byte base58 string with privacy prefix. - #[arg(long, conflicts_with = "from_label")] + #[arg(long, conflicts_with = "from_label", required_unless_present_any = ["from_label", "from_key_path"])] from: Option, /// From account label (alternative to --from). #[arg(long, conflicts_with = "from")] @@ -60,14 +58,18 @@ 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, - #[arg(long, conflicts_with = "from", conflicts_with = "from_label")] - pin: Option, + /// `from_key_path` (alternative to --from) uses Keycard. #[arg(long, conflicts_with = "from", conflicts_with = "from_label")] from_key_path: Option, - #[arg(long, conflicts_with = "from", conflicts_with = "from_label")] + /// `to_key_path` (alternative to --to) uses Keycard. + #[arg(long, conflicts_with = "to", conflicts_with = "to_label")] to_key_path: Option, }, } @@ -81,7 +83,6 @@ impl WalletSubcommand for AuthTransferSubcommand { Self::Init { account_id, account_label, - pin, key_path, } => { let resolved = resolve_id_or_label( @@ -89,8 +90,7 @@ impl WalletSubcommand for AuthTransferSubcommand { account_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &pin, - &key_path, + key_path.as_deref(), )?; let (account_id, addr_privacy) = parse_addr_with_privacy_prefix(&resolved)?; @@ -100,7 +100,7 @@ impl WalletSubcommand for AuthTransferSubcommand { let account_id = account_id.parse()?; let tx_hash = NativeTokenTransfer(wallet_core) - .register_account(account_id, &pin, &key_path) + .register_account(account_id, key_path.as_deref()) .await?; println!("Transaction hash is {tx_hash}"); @@ -144,8 +144,8 @@ impl WalletSubcommand for AuthTransferSubcommand { to_label, to_npk, to_vpk, + to_identifier, amount, - pin, from_key_path, to_key_path, } => { @@ -154,10 +154,10 @@ impl WalletSubcommand for AuthTransferSubcommand { from_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &pin, - &from_key_path, + from_key_path.as_deref(), )?; + let to_key_path_for_sign = to_key_path.clone(); let to = match (to, to_label, to_key_path) { (v, None, None) => v, (None, Some(label), None) => Some(resolve_account_label( @@ -165,12 +165,13 @@ impl WalletSubcommand for AuthTransferSubcommand { &wallet_core.storage.labels, &wallet_core.storage.user_data, )?), - (None, None, Some(to_key_path)) => { - Some(KeycardWallet::get_account_id_for_path_with_connect( - pin.as_ref().expect("`wallet::programs::native_token_transfer::send`: invalid data received for pin"), - &to_key_path, - )) - } + (None, None, Some(to_key_path)) => Some(resolve_id_or_label( + None, + None, + &wallet_core.storage.labels, + &wallet_core.storage.user_data, + Some(&to_key_path), + )?), _ => { anyhow::bail!("Provide only one of --to or --to-label") } @@ -200,8 +201,8 @@ impl WalletSubcommand for AuthTransferSubcommand { from, to, amount, - pin, - key_path: from_key_path, + from_key_path, + to_key_path: to_key_path_for_sign, } } (AccountPrivacyKind::Private, AccountPrivacyKind::Private) => { @@ -247,6 +248,7 @@ impl WalletSubcommand for AuthTransferSubcommand { from, to_npk, to_vpk, + to_identifier, amount, pin: pin.clone(), key_path: from_key_path.clone(), @@ -259,6 +261,7 @@ impl WalletSubcommand for AuthTransferSubcommand { from, to_npk, to_vpk, + to_identifier, amount, pin, key_path: from_key_path, @@ -291,10 +294,10 @@ pub enum NativeTokenTransferProgramSubcommand { /// amount - amount of balance to move. #[arg(long)] amount: u128, - #[arg(long, conflicts_with = "from", conflicts_with = "from_label")] - pin: Option, - #[arg(long, conflicts_with = "from", conflicts_with = "from_label")] - key_path: Option, + #[arg(long)] + from_key_path: Option, + #[arg(skip)] + to_key_path: Option, }, /// Private execution. #[command(subcommand)] @@ -357,6 +360,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, @@ -402,6 +408,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, @@ -453,6 +462,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandPrivate { from, to_npk, to_vpk, + to_identifier, amount, pin, key_path, @@ -537,6 +547,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded { from, to_npk, to_vpk, + to_identifier, amount, pin, key_path, @@ -617,14 +628,20 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommand { from, to, amount, - pin, - key_path, + from_key_path, + to_key_path, } => { let from: AccountId = from.parse().unwrap(); let to: AccountId = to.parse().unwrap(); let tx_hash = NativeTokenTransfer(wallet_core) - .send_public_transfer(from, to, amount, &pin, &key_path) + .send_public_transfer( + from, + to, + amount, + from_key_path.as_deref(), + to_key_path.as_deref(), + ) .await?; println!("Transaction hash is {tx_hash}"); diff --git a/wallet/src/cli/programs/pinata.rs b/wallet/src/cli/programs/pinata.rs index 240a6c81..d8b01959 100644 --- a/wallet/src/cli/programs/pinata.rs +++ b/wallet/src/cli/programs/pinata.rs @@ -17,19 +17,13 @@ pub enum PinataProgramAgnosticSubcommand { /// Claim pinata. Claim { /// to - valid 32 byte base58 string with privacy prefix. - #[arg(long, conflicts_with = "to_label", required_unless_present_any = ["to_label", "pin"])] + #[arg(long, conflicts_with = "to_label", required_unless_present_any = ["to_label", "key_path"])] to: Option, /// To account label (alternative to --to). #[arg(long, conflicts_with = "to")] to_label: Option, - #[arg( - long, - conflicts_with = "to", - conflicts_with = "to_label", - requires = "key_path" - )] - pin: Option, - #[arg(long)] + /// To key path (alternative to --to) uses Keycard. + #[arg(long, conflicts_with = "to", conflicts_with = "to_label")] key_path: Option, }, } @@ -43,7 +37,6 @@ impl WalletSubcommand for PinataProgramAgnosticSubcommand { Self::Claim { to, to_label, - pin, key_path, } => { let to = resolve_id_or_label( @@ -51,8 +44,7 @@ impl WalletSubcommand for PinataProgramAgnosticSubcommand { to_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &pin, - &key_path, + key_path.as_deref(), )?; let (to, to_addr_privacy) = parse_addr_with_privacy_prefix(&to)?; diff --git a/wallet/src/cli/programs/token.rs b/wallet/src/cli/programs/token.rs index 7e4869f0..66fb7bf7 100644 --- a/wallet/src/cli/programs/token.rs +++ b/wallet/src/cli/programs/token.rs @@ -73,11 +73,13 @@ 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, - #[arg(long)] - pin: Option, #[arg(long, conflicts_with = "from", conflicts_with = "from_label")] from_key_path: Option, }, @@ -108,8 +110,6 @@ pub enum TokenProgramAgnosticSubcommand { #[arg(long)] amount: u128, #[arg(long, conflicts_with = "holder", conflicts_with = "holder_label")] - holder_pin: Option, - #[arg(long, conflicts_with = "holder", conflicts_with = "holder_label")] holder_key_path: Option, }, /// Mint tokens on `holder`, modify `definition`. @@ -132,7 +132,7 @@ pub enum TokenProgramAgnosticSubcommand { #[arg(long, conflicts_with = "definition")] definition_label: Option, /// holder - valid 32 byte base58 string with privacy prefix. - #[arg(long, conflicts_with = "holder_label", required_unless_present_any = ["holder_label", "pin"])] + #[arg(long, conflicts_with = "holder_label", required_unless_present_any = ["holder_label", "holder_key_path"])] holder: Option, /// Holder account label (alternative to --holder). #[arg(long, conflicts_with = "holder")] @@ -143,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, @@ -168,16 +172,14 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { definition_account_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, + None, )?; let supply_account_id = resolve_id_or_label( supply_account_id, supply_account_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, + None, )?; let (definition_account_id, definition_addr_privacy) = parse_addr_with_privacy_prefix(&definition_account_id)?; @@ -236,8 +238,8 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { to_label, to_npk, to_vpk, + to_identifier, amount, - pin, from_key_path, } => { let from = resolve_id_or_label( @@ -245,8 +247,7 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { from_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &pin, - &from_key_path, + from_key_path.as_deref(), )?; let to = match (to, to_label) { (v, None) => v, @@ -327,6 +328,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, }, ), @@ -335,6 +337,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, }, ), @@ -350,7 +353,6 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { holder, holder_label, amount, - holder_pin, holder_key_path, } => { let definition = resolve_id_or_label( @@ -358,16 +360,14 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { definition_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, + None, )?; let holder = resolve_id_or_label( holder, holder_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &holder_pin, - &holder_key_path, + holder_key_path.as_deref(), )?; let underlying_subcommand = { let (definition, definition_privacy) = @@ -422,6 +422,7 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { holder_label, holder_npk, holder_vpk, + holder_identifier, amount, } => { let definition = resolve_id_or_label( @@ -429,8 +430,7 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { definition_label, &wallet_core.storage.labels, &wallet_core.storage.user_data, - &None, - &None, + None, )?; let holder = match (holder, holder_label) { (v, None) => v, @@ -511,6 +511,7 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { definition_account_id: definition, holder_npk, holder_vpk, + holder_identifier, amount, }, ), @@ -519,6 +520,7 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { definition_account_id: definition, holder_npk, holder_vpk, + holder_identifier, amount, }, ), @@ -610,6 +612,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, }, @@ -639,6 +644,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, }, @@ -698,6 +706,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, }, @@ -727,6 +738,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, }, @@ -891,6 +905,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(); @@ -911,6 +926,7 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate { sender_account_id, recipient_npk, recipient_vpk, + recipient_identifier.unwrap_or_else(rand::random), balance_to_move, ) .await?; @@ -1008,6 +1024,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(); @@ -1029,6 +1046,7 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate { definition_account_id, holder_npk, holder_vpk, + holder_identifier.unwrap_or_else(rand::random), amount, ) .await?; @@ -1173,6 +1191,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(); @@ -1193,6 +1212,7 @@ impl WalletSubcommand for TokenProgramSubcommandShielded { sender_account_id, recipient_npk, recipient_vpk, + recipient_identifier.unwrap_or_else(rand::random), balance_to_move, ) .await?; @@ -1312,6 +1332,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(); @@ -1333,6 +1354,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 8e5e3f3e..54883fdb 100644 --- a/wallet/src/helperfunctions.rs +++ b/wallet/src/helperfunctions.rs @@ -51,6 +51,20 @@ impl From for HumanReadableAccount { } } +/// Read the Keycard PIN without echoing it. +/// +/// Checks `KEYCARD_PIN` first so non-interactive callers (CI, scripts) can +/// supply it via the environment. Falls back to a TTY prompt via `rpassword` +/// so the value never appears in argv, shell history, or `ps` output. +pub fn read_pin() -> Result> { + if let Ok(pin) = std::env::var("KEYCARD_PIN") { + return Ok(zeroize::Zeroizing::new(pin)); + } + rpassword::prompt_password("Keycard PIN: ") + .map(zeroize::Zeroizing::new) + .map_err(Into::into) +} + /// Resolve an account id-or-label pair to a `Privacy/id` string. /// /// Exactly one of `id` or `label` must be `Some`. If `id` is provided it is @@ -61,16 +75,16 @@ pub fn resolve_id_or_label( label: Option, labels: &HashMap, user_data: &NSSAUserData, - pin: &Option, - key_path: &Option, + key_path: Option<&str>, ) -> Result { - match (id, label, pin) { + match (id, label, key_path) { (Some(id), None, None) => Ok(id), (None, Some(label), None) => resolve_account_label(&label, labels, user_data), - (None, None, Some(pin)) => Ok(KeycardWallet::get_account_id_for_path_with_connect( - pin, - key_path.as_ref().expect("Expect a key path String."), - )), + (None, None, Some(key_path)) => { + let pin = read_pin()?; + KeycardWallet::get_account_id_for_path_with_connect(&pin, key_path) + .map_err(anyhow::Error::from) + } _ => anyhow::bail!("provide exactly one of account id, account label or keycard path"), } } @@ -172,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 { @@ -195,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 6a58ecbb..d655a4ca 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -71,6 +71,8 @@ pub enum ExecutionFailureKind { AccountDataError(AccountId), #[error("Failed to build transaction: {0}")] TransactionBuildError(#[from] nssa::error::NssaError), + #[error(transparent)] + KeycardError(#[from] pyo3::PyErr), } #[expect(clippy::partial_pub_fields, reason = "TODO: make all fields private")] @@ -260,13 +262,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. @@ -299,13 +323,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. @@ -338,7 +363,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, @@ -351,7 +376,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 => {} } @@ -453,7 +478,7 @@ impl WalletCore { visibility_mask, 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(), @@ -473,8 +498,7 @@ impl WalletCore { ) .unwrap(); - let witness_set = Self::sign_privacy_message(&message, &proof, &acc_manager, pin, key_path) - .expect("TODO-Marvin"); + 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 @@ -539,33 +563,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); @@ -577,18 +601,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); } } @@ -631,7 +661,7 @@ impl WalletCore { pub fn sign_privacy_message( message: &nssa::privacy_preserving_transaction::Message, - proof: &Proof, + proof: Proof, acc_manager: &privacy_preserving_tx::AccountManager, pin: &Option, key_path: &Option, diff --git a/wallet/src/privacy_preserving_tx.rs b/wallet/src/privacy_preserving_tx.rs index b62f1c2f..b428450e 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 5244b8cd..546d290c 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}; @@ -37,6 +37,7 @@ impl NativeTokenTransfer<'_> { from: AccountId, to_npk: NullifierPublicKey, to_vpk: ViewingPublicKey, + to_identifier: Identifier, balance_to_move: u128, pin: &Option, key_path: &Option, @@ -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/native_token_transfer/public.rs b/wallet/src/program_facades/native_token_transfer/public.rs index e7790897..69185147 100644 --- a/wallet/src/program_facades/native_token_transfer/public.rs +++ b/wallet/src/program_facades/native_token_transfer/public.rs @@ -5,10 +5,11 @@ use nssa::{ program::Program, public_transaction::{Message, WitnessSet}, }; +use pyo3::exceptions::PyRuntimeError; use sequencer_service_rpc::RpcClient as _; use super::NativeTokenTransfer; -use crate::{ExecutionFailureKind, WalletCore}; +use crate::ExecutionFailureKind; impl NativeTokenTransfer<'_> { pub async fn send_public_transfer( @@ -16,8 +17,8 @@ impl NativeTokenTransfer<'_> { from: AccountId, to: AccountId, balance_to_move: u128, - pin: &Option, - key_path: &Option, + from_key_path: Option<&str>, + to_key_path: Option<&str>, ) -> Result { let balance = self .0 @@ -32,46 +33,39 @@ impl NativeTokenTransfer<'_> { let account_ids = vec![from, to]; let program_id = Program::authenticated_transfer_program().id(); - // Fetch nonces for both accounts unconditionally - let mut nonces = self + let nonces = self .0 - .get_accounts_nonces(vec![from]) + .get_accounts_nonces(account_ids.clone()) .await .map_err(ExecutionFailureKind::SequencerError)?; - let to_signing_key = self.0.storage.user_data.get_pub_account_signing_key(to); - if let Some(_to_signing_key) = to_signing_key { - let to_nonces = self - .0 - .get_accounts_nonces(vec![to]) - .await - .map_err(ExecutionFailureKind::SequencerError)?; - nonces.extend(to_nonces); + + let message = Message::try_new(program_id, account_ids, nonces, balance_to_move) + .map_err(ExecutionFailureKind::TransactionBuildError)?; + + let witness_set = if let Some(from_key_path) = from_key_path { + let pin = crate::helperfunctions::read_pin().map_err(|e| { + ExecutionFailureKind::KeycardError(pyo3::PyErr::new::( + e.to_string(), + )) + })?; + let msg_hash = message.hash_message(); + let (from_sig, from_pk) = + KeycardWallet::sign_message_for_path_with_connect(&pin, from_key_path, &msg_hash)?; + if let Some(to_key_path) = to_key_path { + let (to_sig, to_pk) = KeycardWallet::sign_message_for_path_with_connect( + &pin, + to_key_path, + &msg_hash, + )?; + WitnessSet::from_list(&message, &[from_sig, to_sig], &[from_pk, to_pk]) + .map_err(ExecutionFailureKind::TransactionBuildError)? + } else { + WitnessSet::from_list(&message, &[from_sig], &[from_pk]) + .map_err(ExecutionFailureKind::TransactionBuildError)? + } } else { - println!( - "Receiver's account ({to}) private key not found in wallet. Proceeding with only sender's key." - ); - } - - let message = Message::try_new(program_id, account_ids, nonces, balance_to_move).unwrap(); - - let witness_set = pin.as_ref().map_or_else(|| { - let sign_ids = self.0.filter_owned_accounts(&[from, to]); - WalletCore::sign_public_message(self.0, &message, &sign_ids) - .expect("`WalletCore::sign_public_message() failed to produce a signature for a NativeTokenTransfer.") - }, |pin| { - let key_path = key_path.as_ref().expect("`NativeTokenTransfer::send_public_transfer() expected a String for `key_path`."); - let pub_key = KeycardWallet::get_public_key_for_path_with_connect( - pin, - key_path, - ); - let signature = KeycardWallet::sign_message_for_path_with_connect( - pin, - key_path, - &message.hash_message(), - ) - .expect("`NativeTokenTransfer::send_public_transfer() failed to produce a Signature for the given `pin` and `key_path`."); - WitnessSet::from_list(&[signature], &[pub_key]) - }); + self.0.sign_public_message(&message, &message.account_ids)? + }; let tx = PublicTransaction::new(message, witness_set); @@ -85,8 +79,7 @@ impl NativeTokenTransfer<'_> { pub async fn register_account( &self, from: AccountId, - pin: &Option, // Used by Keycard. - key_path: &Option, // Used by Keycard. + key_path: Option<&str>, ) -> Result { let nonces = self .0 @@ -98,9 +91,22 @@ impl NativeTokenTransfer<'_> { let account_ids = vec![from]; let program_id = Program::authenticated_transfer_program().id(); let message = Message::try_new(program_id, account_ids, nonces, instruction) - .expect("Expect a valid Message"); + .map_err(ExecutionFailureKind::TransactionBuildError)?; - let witness_set = if pin.is_none() { + let witness_set = if let Some(key_path) = key_path { + let pin = crate::helperfunctions::read_pin().map_err(|e| { + ExecutionFailureKind::KeycardError(pyo3::PyErr::new::( + e.to_string(), + )) + })?; + let (signature, pub_key) = KeycardWallet::sign_message_for_path_with_connect( + &pin, + key_path, + &message.hash_message(), + )?; + WitnessSet::from_list(&message, &[signature], &[pub_key]) + .map_err(ExecutionFailureKind::TransactionBuildError)? + } else { let signing_key = self.0.storage.user_data.get_pub_account_signing_key(from); let Some(signing_key) = signing_key else { @@ -108,19 +114,6 @@ impl NativeTokenTransfer<'_> { }; WitnessSet::for_message(&message, &[signing_key]) - } else { - let pub_key = KeycardWallet::get_public_key_for_path_with_connect( - pin.as_ref().expect("`wallet::program_facades::native_token_transfer::public::register_account`: invalid data received for pin for public key"), - key_path.as_ref().expect("`wallet::program_facades::native_token_transfer::public::register_account`: invalid data received for key_path for public_key"), - ); - - let signature = KeycardWallet::sign_message_for_path_with_connect( - pin.as_ref().as_ref().expect("`wallet::program_facades::native_token_transfer::public::register_account`: invalid data received for pin for signature"), - key_path.as_ref().expect("`wallet::program_facades::native_token_transfer::public::register_account`: invalid data received for key_path for public_key"), - &message.hash_message(), - ) - .expect("Expect a valid Signature."); - WitnessSet::from_list(&[signature], &[pub_key]) }; 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 48d90820..59b6a306 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}; @@ -43,6 +43,7 @@ impl NativeTokenTransfer<'_> { from: AccountId, to_npk: NullifierPublicKey, to_vpk: ViewingPublicKey, + to_identifier: Identifier, balance_to_move: u128, pin: &Option, from_key_path: &Option, @@ -56,6 +57,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 92ba6ec0..c2aa5145 100644 --- a/wallet/src/program_facades/token.rs +++ b/wallet/src/program_facades/token.rs @@ -272,6 +272,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 { @@ -287,6 +288,7 @@ impl Token<'_> { PrivacyPreservingAccount::PrivateForeign { npk: recipient_npk, vpk: recipient_vpk, + identifier: recipient_identifier, }, ], instruction_data, @@ -374,6 +376,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 { @@ -389,6 +392,7 @@ impl Token<'_> { PrivacyPreservingAccount::PrivateForeign { npk: recipient_npk, vpk: recipient_vpk, + identifier: recipient_identifier, }, ], instruction_data, @@ -647,6 +651,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 { @@ -662,6 +667,7 @@ impl Token<'_> { PrivacyPreservingAccount::PrivateForeign { npk: holder_npk, vpk: holder_vpk, + identifier: holder_identifier, }, ], instruction_data, @@ -749,6 +755,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 { @@ -764,6 +771,7 @@ impl Token<'_> { PrivacyPreservingAccount::PrivateForeign { npk: holder_npk, vpk: holder_vpk, + identifier: holder_identifier, }, ], instruction_data,