diff --git a/Cargo.lock b/Cargo.lock index aa87d011..79917fac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8989,9 +8989,9 @@ dependencies = [ "amm_core", "anyhow", "async-stream", + "base58", "base64 0.22.1", "borsh", - "bytemuck", "clap", "common", "env_logger", diff --git a/Justfile b/Justfile index db167d26..c8ffe09d 100644 --- a/Justfile +++ b/Justfile @@ -37,9 +37,15 @@ run-sequencer: # Run Indexer [working-directory: 'indexer/service'] -run-indexer: +run-indexer mock="": @echo "๐Ÿ” Running indexer" - RUST_LOG=info RISC0_DEV_MODE=1 cargo run --release -p indexer_service configs/indexer_config.json + @if [ "{{mock}}" = "mock" ]; then \ + echo "๐Ÿงช Using mock data"; \ + RUST_LOG=info RISC0_DEV_MODE=1 cargo run --release --features mock-responses -p indexer_service configs/indexer_config.json; \ + else \ + echo "๐Ÿš€ Using real data"; \ + RUST_LOG=info RISC0_DEV_MODE=1 cargo run --release -p indexer_service configs/indexer_config.json; \ + fi # Run Explorer [working-directory: 'explorer_service'] @@ -58,4 +64,6 @@ clean: @echo "๐Ÿงน Cleaning run artifacts" rm -rf sequencer_runner/bedrock_signing_key rm -rf sequencer_runner/rocksdb + rm -rf indexer/service/rocksdb rm -rf wallet/configs/debug/storage.json + cd bedrock && docker compose down -v diff --git a/README.md b/README.md index ee2d8097..b0086f4a 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ Both public and private executions use the same Risc0 VM bytecode. Public transa This design keeps public transactions as fast as any RISC-Vโ€“based VM and makes private transactions efficient for validators. It also supports parallel execution similar to Solana, improving throughput. The main computational cost for privacy-preserving transactions is on the user side, where ZK proofs are generated. +--- --- --- @@ -130,29 +131,31 @@ RUST_LOG=info RISC0_DEV_MODE=1 cargo run $(pwd)/configs/debug all ``` # Run the sequencer and node - - ## Running Manually ### Normal mode The sequencer and logos blockchain node can be run locally: 1. On one terminal go to the `logos-blockchain/logos-blockchain` repo and run a local logos blockchain node: - - `git checkout master; git pull` - - `cargo clean` - - `rm -r ~/.logos-blockchain-circuits` - - `./scripts/setup-logos-blockchain-circuits.sh` - - `cargo build --all-features` - - `./target/debug/logos-blockchain-node --deployment nodes/node/standalone-deployment-config.yaml nodes/node/standalone-node-config.yaml` - - 2. Alternatively (WARNING: This node is outdated) go to ``logos-blockchain/lssa/` repo and run the node from docker: - - `cd bedrock` - - Change line 14 of `docker-compose.yml` from `"0:18080/tcp"` into `"8080:18080/tcp"` - - `docker compose up` - - 3. On another terminal go to the `logos-blockchain/lssa` repo and run indexer service: + - `git checkout master; git pull` + - `cargo clean` + - `rm -r ~/.logos-blockchain-circuits` + - `./scripts/setup-logos-blockchain-circuits.sh` + - `cargo build --all-features` + - `./target/debug/logos-blockchain-node --deployment nodes/node/standalone-deployment-config.yaml nodes/node/standalone-node-config.yaml` + + - Alternatively (WARNING: This node is outdated) go to `logos-blockchain/lssa/` repo and run the node from docker: + - `cd bedrock` + - Change line 14 of `docker-compose.yml` from `"0:18080/tcp"` into `"8080:18080/tcp"` + - `docker compose up` + + 2. On another terminal go to the `logos-blockchain/lssa` repo and run indexer service: - `RUST_LOG=info cargo run -p indexer_service indexer/service/configs/indexer_config.json` - 4. On another terminal go to the `logos-blockchain/lssa` repo and run the sequencer: + 3. On another terminal go to the `logos-blockchain/lssa` repo and run the sequencer: - `RUST_LOG=info cargo run -p sequencer_runner sequencer_runner/configs/debug` + 4. (To run the explorer): on another terminal go to `logos-blockchain/lssa/explorer_service` and run the following: + - `cargo install cargo-leptos` + - `cargo leptos build --release` + - `cargo leptos serve --release` ### Notes on cleanup diff --git a/artifacts/program_methods/amm.bin b/artifacts/program_methods/amm.bin index 1f9a26d0..da0ed533 100644 Binary files a/artifacts/program_methods/amm.bin and b/artifacts/program_methods/amm.bin differ diff --git a/artifacts/program_methods/authenticated_transfer.bin b/artifacts/program_methods/authenticated_transfer.bin index 3f7a41a6..056d6ff8 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/pinata.bin b/artifacts/program_methods/pinata.bin index 02501289..7c7c66a5 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 3d4b4626..d0073e59 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 6b4ace2d..e5131ebe 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 9391203b..edd10086 100644 Binary files a/artifacts/program_methods/token.bin and b/artifacts/program_methods/token.bin differ diff --git a/artifacts/test_program_methods/burner.bin b/artifacts/test_program_methods/burner.bin index 0647ee76..bba11c26 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 6400576e..d0b28ca6 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 15bc00aa..1189fafd 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 92d28ba6..459b4049 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/data_changer.bin b/artifacts/test_program_methods/data_changer.bin index 72090321..44ffa8cb 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 c46fb73d..bfb0beda 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/malicious_authorization_changer.bin b/artifacts/test_program_methods/malicious_authorization_changer.bin index e3c89fc9..961d3bd1 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/minter.bin b/artifacts/test_program_methods/minter.bin index 83a6d0e6..7f59d988 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 6f69a5d3..af956dfd 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 128265eb..48b55139 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 3c3b3784..cdb931f9 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 94ea332c..6a90fcd4 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/program_owner_changer.bin b/artifacts/test_program_methods/program_owner_changer.bin index f82633a1..d85fb360 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 9bb3c071..3e87f257 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/common/src/lib.rs b/common/src/lib.rs index 11fa4c3d..972aeadc 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -17,7 +17,6 @@ pub mod test_utils; pub const PINATA_BASE58: &str = "EfQhKQAkX2FJiwNii2WFQsGndjvF1Mzd7RuVe7QdPLw7"; #[derive( - Debug, Default, Copy, Clone, @@ -37,6 +36,12 @@ impl Display for HashType { } } +impl std::fmt::Debug for HashType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", hex::encode(self.0)) + } +} + impl FromStr for HashType { type Err = hex::FromHexError; diff --git a/completions/README.md b/completions/README.md index 4695c9e6..d274774c 100644 --- a/completions/README.md +++ b/completions/README.md @@ -1,6 +1,6 @@ # Wallet CLI Completion -Completion scripts for the LSSA `wallet` command. +Completion scripts for the LSSA `wallet` command. ## ZSH @@ -19,9 +19,9 @@ Preconfigured accounts and accounts only with `/` (no number) are not completed. e.g.: ``` -โ–ถ wallet account list -Preconfigured Public/Gj1mJy5W7J5pfmLRujmQaLfLMWidNxQ6uwnhb666ZwHw, -Preconfigured Public/BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy, +โ–ถ wallet account list +Preconfigured Public/7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo, +Preconfigured Public/6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV, Preconfigured Private/3oCG8gqdKLMegw4rRfyaMQvuPHpcASt7xwttsmnZLSkw, Preconfigured Private/AKTcXgJ1xoynta1Ec7y6Jso1z1JQtHqd7aPQ1h9er6xX, / Public/8DstRgMQrB2N9a7ymv98RDDbt8nctrP9ZzaNRSpKDZSu, @@ -118,9 +118,80 @@ wallet account get --account-id # Shows: Public/... Private/... ``` +## Bash + +Works with bash 4+. The `bash-completion` package is required for auto-sourcing from +`/etc/bash_completion.d/`; without it, source the file directly from `~/.bashrc` instead. + +### Features + +- Full completion for all wallet subcommands +- Contextual option completion for each command +- Dynamic account ID completion via `wallet account list` +- Falls back to `Public/` / `Private/` prefixes when no accounts are available + +Note that only accounts created by the user auto-complete (same filtering as zsh โ€” see above). + +### Installation + +#### Option A โ€” source directly from `~/.bashrc` (works everywhere) + +```sh +echo "source $(pwd)/completions/bash/wallet" >> ~/.bashrc +exec bash +``` + +#### Option B โ€” system-wide via `bash-completion` + +1. Copy the file: + + ```sh + cp ./bash/wallet /etc/bash_completion.d/wallet + ``` + +2. Ensure `bash-completion` is initialised in every interactive shell. On many Linux + distributions (e.g. Fedora) it is only sourced for **login** shells via + `/etc/profile.d/bash_completion.sh`. For non-login shells (e.g. a bash session started + inside zsh), add this to `~/.bashrc`: + + ```sh + [[ -f /usr/share/bash-completion/bash_completion ]] && source /usr/share/bash-completion/bash_completion + ``` + +3. Reload your shell: + + ```sh + exec bash + ``` + +### Requirements + +The completion script calls `wallet account list` to dynamically fetch account IDs. Ensure the `wallet` command is in your `$PATH`. + +### Usage + +```sh +# Main commands +wallet + +# Account subcommands +wallet account + +# Options for auth-transfer send +wallet auth-transfer send -- + +# Account types when creating +wallet account new +# Shows: public private + +# Account IDs (fetched dynamically) +wallet account get --account-id +# Shows: Public/... Private/... +``` + ## Troubleshooting -### Completions not appearing +### Zsh completions not appearing 1. Check that `compinit` is called in your `.zshrc` 2. Rebuild the completion cache: diff --git a/completions/bash/wallet b/completions/bash/wallet new file mode 100644 index 00000000..57dd3636 --- /dev/null +++ b/completions/bash/wallet @@ -0,0 +1,382 @@ +#!/usr/bin/env bash + +# Bash completion script for the wallet CLI +# See instructions in ../README.md + +# Helper function to complete account IDs +# Uses `wallet account list` to get available accounts +# Only includes accounts with /N prefix (where N is a number) +_wallet_complete_account_id() { + local cur="$1" + local accounts + + if command -v wallet &>/dev/null; then + accounts=$(wallet account list 2>/dev/null | grep '^/[0-9]' | awk '{print $2}' | tr -d ',') + fi + + if [[ -n "$accounts" ]]; then + COMPREPLY=($(compgen -W "$accounts" -- "$cur")) + else + COMPREPLY=($(compgen -W "Public/ Private/" -- "$cur")) + compopt -o nospace 2>/dev/null + fi +} + +_wallet() { + local cur prev words cword + _init_completion 2>/dev/null || { + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + words=("${COMP_WORDS[@]}") + cword=$COMP_CWORD + } + + local commands="auth-transfer chain-info account pinata token amm 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. + local cmd="" subcmd="" + local cmd_idx=0 subcmd_idx=0 + local i + for ((i = 1; i < cword; i++)); do + local w="${words[$i]}" + case "$w" in + --auth) + ((i++)) # skip the auth value + ;; + -c | --continuous-run) + # boolean flag, no value + ;; + -*) + # unrecognised option, skip + ;; + *) + if [[ -z "$cmd" ]]; then + cmd="$w" + cmd_idx=$i + elif [[ -z "$subcmd" ]]; then + subcmd="$w" + subcmd_idx=$i + fi + ;; + esac + done + + local config_keys="override_rust_log sequencer_addr seq_poll_timeout seq_tx_poll_max_blocks seq_poll_max_retries seq_block_poll_max_amount initial_accounts basic_auth" + + case "$cmd" in + "") + # Completing the main command or a global option + if [[ "$prev" == "--auth" ]]; then + return # completing the --auth value; no suggestions + fi + case "$cur" in + -*) + COMPREPLY=($(compgen -W "-c --continuous-run --auth" -- "$cur")) + ;; + *) + COMPREPLY=($(compgen -W "$commands" -- "$cur")) + ;; + esac + ;; + + auth-transfer) + case "$subcmd" in + "") + COMPREPLY=($(compgen -W "init send help" -- "$cur")) + ;; + init) + case "$prev" in + --account-id) + _wallet_complete_account_id "$cur" + ;; + *) + COMPREPLY=($(compgen -W "--account-id" -- "$cur")) + ;; + esac + ;; + send) + case "$prev" in + --from | --to) + _wallet_complete_account_id "$cur" + ;; + --to-npk | --to-vpk | --amount) + ;; # no specific completion + *) + COMPREPLY=($(compgen -W "--from --to --to-npk --to-vpk --amount" -- "$cur")) + ;; + esac + ;; + esac + ;; + + chain-info) + case "$subcmd" in + "") + COMPREPLY=($(compgen -W "current-block-id block transaction help" -- "$cur")) + ;; + block) + case "$prev" in + -i | --id) + ;; # no specific completion for block ID + *) + COMPREPLY=($(compgen -W "-i --id" -- "$cur")) + ;; + esac + ;; + transaction) + case "$prev" in + -t | --hash) + ;; # no specific completion for tx hash + *) + COMPREPLY=($(compgen -W "-t --hash" -- "$cur")) + ;; + esac + ;; + esac + ;; + + account) + case "$subcmd" in + "") + COMPREPLY=($(compgen -W "get new sync-private list ls label help" -- "$cur")) + ;; + get) + case "$prev" in + -a | --account-id) + _wallet_complete_account_id "$cur" + ;; + *) + COMPREPLY=($(compgen -W "-r --raw -k --keys -a --account-id" -- "$cur")) + ;; + esac + ;; + list | ls) + COMPREPLY=($(compgen -W "-l --long" -- "$cur")) + ;; + sync-private) + ;; # no options + new) + # `account new` is itself a subcommand: public | private + local new_subcmd="" + for ((i = subcmd_idx + 1; i < cword; i++)); do + case "${words[$i]}" in + public | private) + new_subcmd="${words[$i]}" + break + ;; + esac + done + + if [[ -z "$new_subcmd" ]]; then + COMPREPLY=($(compgen -W "public private" -- "$cur")) + else + case "$prev" in + --cci | -l | --label) + ;; # no specific completion + *) + COMPREPLY=($(compgen -W "--cci -l --label" -- "$cur")) + ;; + esac + fi + ;; + label) + case "$prev" in + -a | --account-id) + _wallet_complete_account_id "$cur" + ;; + -l | --label) + ;; # no specific completion for label value + *) + COMPREPLY=($(compgen -W "-a --account-id -l --label" -- "$cur")) + ;; + esac + ;; + esac + ;; + + pinata) + case "$subcmd" in + "") + COMPREPLY=($(compgen -W "claim help" -- "$cur")) + ;; + claim) + case "$prev" in + --to) + _wallet_complete_account_id "$cur" + ;; + *) + COMPREPLY=($(compgen -W "--to" -- "$cur")) + ;; + esac + ;; + esac + ;; + + token) + case "$subcmd" in + "") + COMPREPLY=($(compgen -W "new send burn mint help" -- "$cur")) + ;; + new) + case "$prev" in + --definition-account-id | --supply-account-id) + _wallet_complete_account_id "$cur" + ;; + -n | --name | -t | --total-supply) + ;; # no specific completion + *) + COMPREPLY=($(compgen -W "--definition-account-id --supply-account-id -n --name -t --total-supply" -- "$cur")) + ;; + esac + ;; + send) + case "$prev" in + --from | --to) + _wallet_complete_account_id "$cur" + ;; + --to-npk | --to-vpk | --amount) + ;; # no specific completion + *) + COMPREPLY=($(compgen -W "--from --to --to-npk --to-vpk --amount" -- "$cur")) + ;; + esac + ;; + burn) + case "$prev" in + --definition | --holder) + _wallet_complete_account_id "$cur" + ;; + --amount) + ;; # no specific completion + *) + COMPREPLY=($(compgen -W "--definition --holder --amount" -- "$cur")) + ;; + esac + ;; + mint) + case "$prev" in + --definition | --holder) + _wallet_complete_account_id "$cur" + ;; + --holder-npk | --holder-vpk | --amount) + ;; # no specific completion + *) + COMPREPLY=($(compgen -W "--definition --holder --holder-npk --holder-vpk --amount" -- "$cur")) + ;; + esac + ;; + esac + ;; + + amm) + case "$subcmd" in + "") + COMPREPLY=($(compgen -W "new swap add-liquidity remove-liquidity help" -- "$cur")) + ;; + new) + case "$prev" in + --user-holding-a | --user-holding-b | --user-holding-lp) + _wallet_complete_account_id "$cur" + ;; + --balance-a | --balance-b) + ;; # no specific completion + *) + COMPREPLY=($(compgen -W "--user-holding-a --user-holding-b --user-holding-lp --balance-a --balance-b" -- "$cur")) + ;; + esac + ;; + swap) + case "$prev" in + --user-holding-a | --user-holding-b) + _wallet_complete_account_id "$cur" + ;; + --amount-in | --min-amount-out | --token-definition) + ;; # no specific completion + *) + COMPREPLY=($(compgen -W "--user-holding-a --user-holding-b --amount-in --min-amount-out --token-definition" -- "$cur")) + ;; + esac + ;; + add-liquidity) + case "$prev" in + --user-holding-a | --user-holding-b | --user-holding-lp) + _wallet_complete_account_id "$cur" + ;; + --max-amount-a | --max-amount-b | --min-amount-lp) + ;; # no specific completion + *) + COMPREPLY=($(compgen -W "--user-holding-a --user-holding-b --user-holding-lp --max-amount-a --max-amount-b --min-amount-lp" -- "$cur")) + ;; + esac + ;; + remove-liquidity) + case "$prev" in + --user-holding-a | --user-holding-b | --user-holding-lp) + _wallet_complete_account_id "$cur" + ;; + --balance-lp | --min-amount-a | --min-amount-b) + ;; # no specific completion + *) + COMPREPLY=($(compgen -W "--user-holding-a --user-holding-b --user-holding-lp --balance-lp --min-amount-a --min-amount-b" -- "$cur")) + ;; + esac + ;; + esac + ;; + + config) + case "$subcmd" in + "") + COMPREPLY=($(compgen -W "get set description help" -- "$cur")) + ;; + get) + # Accepts optional -a/--all flag and an optional positional key + COMPREPLY=($(compgen -W "--all -a $config_keys" -- "$cur")) + ;; + set) + # set โ€” only complete the key; no completion for the value + local set_args=0 + for ((i = subcmd_idx + 1; i < cword; i++)); do + [[ "${words[$i]}" != -* ]] && ((set_args++)) + done + if [[ $set_args -eq 0 ]]; then + COMPREPLY=($(compgen -W "$config_keys" -- "$cur")) + fi + ;; + description) + # description โ€” only complete if no key provided yet + local has_key=false + for ((i = subcmd_idx + 1; i < cword; i++)); do + [[ "${words[$i]}" != -* ]] && has_key=true && break + done + if ! $has_key; then + COMPREPLY=($(compgen -W "$config_keys" -- "$cur")) + fi + ;; + esac + ;; + + restore-keys) + case "$prev" in + -d | --depth) + ;; # no specific completion for depth value + *) + COMPREPLY=($(compgen -W "-d --depth" -- "$cur")) + ;; + esac + ;; + + deploy-program) + COMPREPLY=($(compgen -f -- "$cur")) + compopt -o filenames 2>/dev/null + ;; + + help) + COMPREPLY=($(compgen -W "$commands" -- "$cur")) + ;; + esac +} + +complete -F _wallet wallet diff --git a/completions/zsh/_wallet b/completions/zsh/_wallet index e0c5f415..6e60cc53 100644 --- a/completions/zsh/_wallet +++ b/completions/zsh/_wallet @@ -181,7 +181,8 @@ _wallet_account() { ;; new_args) _arguments \ - '--cci[Chain index of a parent node]:chain_index:' + '--cci[Chain index of a parent node]:chain_index:' \ + '(-l --label)'{-l,--label}'[Label to assign to the new account]:label:' ;; esac ;; @@ -343,7 +344,6 @@ _wallet_config() { local -a config_keys config_keys=( - 'all' 'override_rust_log' 'sequencer_addr' 'seq_poll_timeout' @@ -370,7 +370,12 @@ _wallet_config() { ;; args) case $line[1] in - get|description) + get) + _arguments \ + '(-a --all)'{-a,--all}'[Print all config fields]' \ + '::key:compadd -a config_keys' + ;; + description) compadd -a config_keys ;; set) diff --git a/configs/docker-all-in-one/indexer/indexer_config.json b/configs/docker-all-in-one/indexer/indexer_config.json index 610429fb..c2b07e3e 100644 --- a/configs/docker-all-in-one/indexer/indexer_config.json +++ b/configs/docker-all-in-one/indexer/indexer_config.json @@ -1,6 +1,6 @@ { "home": "./indexer/service", - "consensus_info_polling_interval": "60s", + "consensus_info_polling_interval": "1s", "bedrock_client_config": { "addr": "http://logos-blockchain-node-0:18080", "backoff": { @@ -11,50 +11,50 @@ "channel_id": "0101010101010101010101010101010101010101010101010101010101010101", "initial_accounts": [ { - "account_id": "BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy", + "account_id": "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV", "balance": 10000 }, { - "account_id": "Gj1mJy5W7J5pfmLRujmQaLfLMWidNxQ6uwnhb666ZwHw", + "account_id": "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo", "balance": 20000 } ], "initial_commitments": [ { - "npk": [ - 63, - 202, - 178, - 231, - 183, - 82, - 237, - 212, - 216, - 221, - 215, - 255, - 153, - 101, + "npk":[ 177, - 161, - 254, - 210, - 128, - 122, - 54, - 190, - 230, - 151, - 183, 64, - 225, - 229, - 113, 1, - 228, - 97 - ], + 11, + 87, + 38, + 254, + 159, + 231, + 165, + 1, + 94, + 64, + 137, + 243, + 76, + 249, + 101, + 251, + 129, + 33, + 101, + 189, + 30, + 42, + 11, + 191, + 34, + 103, + 186, + 227, + 230 + ] , "account": { "program_owner": [ 0, @@ -73,38 +73,38 @@ }, { "npk": [ - 192, - 251, - 166, - 243, - 167, - 236, - 84, - 249, - 35, + 32, + 67, + 72, + 164, + 106, + 53, + 66, + 239, + 141, + 15, + 52, + 230, 136, - 130, - 172, - 219, - 225, - 161, - 139, - 229, - 89, + 177, + 2, + 236, + 207, 243, - 125, + 134, + 135, + 210, + 143, + 87, + 232, + 215, + 128, 194, - 213, - 209, - 30, - 23, - 174, - 100, - 244, - 124, - 74, - 140, - 47 + 120, + 113, + 224, + 4, + 165 ], "account": { "program_owner": [ diff --git a/configs/docker-all-in-one/sequencer/sequencer_config.json b/configs/docker-all-in-one/sequencer/sequencer_config.json index 7217bf5a..8fc34911 100644 --- a/configs/docker-all-in-one/sequencer/sequencer_config.json +++ b/configs/docker-all-in-one/sequencer/sequencer_config.json @@ -20,17 +20,50 @@ "indexer_rpc_url": "ws://indexer_service:8779", "initial_accounts": [ { - "account_id": "BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy", + "account_id": "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV", "balance": 10000 }, { - "account_id": "Gj1mJy5W7J5pfmLRujmQaLfLMWidNxQ6uwnhb666ZwHw", + "account_id": "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo", "balance": 20000 } ], "initial_commitments": [ { - "npk": [13, 25, 40, 5, 198, 248, 210, 248, 237, 121, 124, 145, 186, 142, 253, 216, 236, 69, 193, 32, 166, 167, 49, 133, 172, 111, 159, 46, 84, 17, 157, 23], + "npk":[ + 177, + 64, + 1, + 11, + 87, + 38, + 254, + 159, + 231, + 165, + 1, + 94, + 64, + 137, + 243, + 76, + 249, + 101, + 251, + 129, + 33, + 101, + 189, + 30, + 42, + 11, + 191, + 34, + 103, + 186, + 227, + 230 + ] , "account": { "program_owner": [ 0, @@ -48,7 +81,40 @@ } }, { - "npk": [32, 67, 72, 164, 106, 53, 66, 239, 141, 15, 52, 230, 136, 177, 2, 236, 207, 243, 134, 135, 210, 143, 87, 232, 215, 128, 194, 120, 113, 224, 4, 165], + "npk": [ + 32, + 67, + 72, + 164, + 106, + 53, + 66, + 239, + 141, + 15, + 52, + 230, + 136, + 177, + 2, + 236, + 207, + 243, + 134, + 135, + 210, + 143, + 87, + 232, + 215, + 128, + 194, + 120, + 113, + 224, + 4, + 165 + ], "account": { "program_owner": [ 0, diff --git a/explorer_service/src/api.rs b/explorer_service/src/api.rs index 4dfd75cd..1e2bdd5b 100644 --- a/explorer_service/src/api.rs +++ b/explorer_service/src/api.rs @@ -118,11 +118,11 @@ pub async fn get_transaction(tx_hash: HashType) -> Result Result, ServerFnError> { +pub async fn get_blocks(before: Option, limit: u32) -> Result, ServerFnError> { use indexer_service_rpc::RpcClient as _; let client = expect_context::(); client - .get_blocks(offset, limit) + .get_blocks(before, limit) .await .map_err(|e| ServerFnError::ServerError(format!("RPC error: {}", e))) } diff --git a/explorer_service/src/pages/main_page.rs b/explorer_service/src/pages/main_page.rs index 3cfb832d..28d1d1d9 100644 --- a/explorer_service/src/pages/main_page.rs +++ b/explorer_service/src/pages/main_page.rs @@ -7,7 +7,7 @@ use crate::{ components::{AccountPreview, BlockPreview, TransactionPreview}, }; -const RECENT_BLOCKS_LIMIT: u64 = 10; +const RECENT_BLOCKS_LIMIT: u32 = 10; /// Main page component #[component] @@ -39,23 +39,57 @@ pub fn MainPage() -> impl IntoView { } }); + // Pagination state for blocks + let (all_blocks, set_all_blocks) = signal(Vec::new()); + let (is_loading_blocks, set_is_loading_blocks) = signal(false); + let (has_more_blocks, set_has_more_blocks) = signal(true); + let (oldest_loaded_block_id, set_oldest_loaded_block_id) = signal(None::); + // Load recent blocks on mount let recent_blocks_resource = Resource::new( || (), - |_| async { - match api::get_latest_block_id().await { - Ok(last_id) => { - api::get_blocks( - std::cmp::max(last_id.saturating_sub(RECENT_BLOCKS_LIMIT) as u32, 1), - (RECENT_BLOCKS_LIMIT + 1) as u32, - ) - .await - } - Err(err) => Err(err), - } - }, + |_| async { api::get_blocks(None, RECENT_BLOCKS_LIMIT).await }, ); + // Update all_blocks when initial load completes + Effect::new(move || { + if let Some(Ok(blocks)) = recent_blocks_resource.get() { + let oldest_id = blocks.last().map(|b| b.header.block_id); + set_all_blocks.set(blocks.clone()); + set_oldest_loaded_block_id.set(oldest_id); + set_has_more_blocks + .set(blocks.len() as u32 == RECENT_BLOCKS_LIMIT && oldest_id.unwrap_or(0) > 1); + } + }); + + // Load more blocks handler + let load_more_blocks = move |_| { + let before_id = oldest_loaded_block_id.get(); + + if before_id.is_none() { + return; + } + + set_is_loading_blocks.set(true); + + leptos::task::spawn_local(async move { + match api::get_blocks(before_id, RECENT_BLOCKS_LIMIT).await { + Ok(new_blocks) => { + let blocks_count = new_blocks.len() as u32; + let new_oldest_id = new_blocks.last().map(|b| b.header.block_id); + set_all_blocks.update(|blocks| blocks.extend(new_blocks)); + set_oldest_loaded_block_id.set(new_oldest_id); + set_has_more_blocks + .set(blocks_count == RECENT_BLOCKS_LIMIT && new_oldest_id.unwrap_or(0) > 1); + } + Err(e) => { + log::error!("Failed to load more blocks: {}", e); + } + } + set_is_loading_blocks.set(false); + }); + }; + // Handle search - update URL parameter let on_search = move |ev: SubmitEvent| { ev.prevent_default(); @@ -196,19 +230,48 @@ pub fn MainPage() -> impl IntoView { recent_blocks_resource .get() .map(|result| match result { - Ok(blocks) if !blocks.is_empty() => { - view! { -
- {blocks - .into_iter() - .map(|block| view! { }) - .collect::>()} -
- } - .into_any() - } Ok(_) => { - view! {
"No blocks found"
}.into_any() + let blocks = all_blocks.get(); + if blocks.is_empty() { + view! {
"No blocks found"
} + .into_any() + } else { + view! { +
+
+ {blocks + .into_iter() + .map(|block| view! { }) + .collect::>()} +
+ {move || { + if has_more_blocks.get() { + view! { + + } + .into_any() + } else { + ().into_any() + } + }} + +
+ } + .into_any() + } } Err(e) => { view! {
{format!("Error: {}", e)}
} diff --git a/indexer/core/src/block_store.rs b/indexer/core/src/block_store.rs index ce3881bd..681b63c8 100644 --- a/indexer/core/src/block_store.rs +++ b/indexer/core/src/block_store.rs @@ -50,8 +50,8 @@ impl IndexerStore { Ok(self.dbio.get_block(id)?) } - pub fn get_block_batch(&self, offset: u64, limit: u64) -> Result> { - Ok(self.dbio.get_block_batch(offset, limit)?) + pub fn get_block_batch(&self, before: Option, limit: u64) -> Result> { + Ok(self.dbio.get_block_batch(before, limit)?) } pub fn get_transaction_by_hash(&self, tx_hash: [u8; 32]) -> Result { diff --git a/indexer/service/configs/indexer_config.json b/indexer/service/configs/indexer_config.json index 247caa8e..bcefffce 100644 --- a/indexer/service/configs/indexer_config.json +++ b/indexer/service/configs/indexer_config.json @@ -1,6 +1,6 @@ { - "home": "./indexer/service", - "consensus_info_polling_interval": "60s", + "home": ".", + "consensus_info_polling_interval": "1s", "bedrock_client_config": { "addr": "http://localhost:8080", "backoff": { @@ -11,50 +11,50 @@ "channel_id": "0101010101010101010101010101010101010101010101010101010101010101", "initial_accounts": [ { - "account_id": "BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy", + "account_id": "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV", "balance": 10000 }, { - "account_id": "Gj1mJy5W7J5pfmLRujmQaLfLMWidNxQ6uwnhb666ZwHw", + "account_id": "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo", "balance": 20000 } ], "initial_commitments": [ { - "npk": [ - 63, - 202, - 178, - 231, - 183, - 82, - 237, - 212, - 216, - 221, - 215, - 255, - 153, - 101, + "npk":[ 177, - 161, - 254, - 210, - 128, - 122, - 54, - 190, - 230, - 151, - 183, 64, - 225, - 229, - 113, 1, - 228, - 97 - ], + 11, + 87, + 38, + 254, + 159, + 231, + 165, + 1, + 94, + 64, + 137, + 243, + 76, + 249, + 101, + 251, + 129, + 33, + 101, + 189, + 30, + 42, + 11, + 191, + 34, + 103, + 186, + 227, + 230 + ] , "account": { "program_owner": [ 0, @@ -73,38 +73,38 @@ }, { "npk": [ - 192, - 251, - 166, - 243, - 167, - 236, - 84, - 249, - 35, + 32, + 67, + 72, + 164, + 106, + 53, + 66, + 239, + 141, + 15, + 52, + 230, 136, - 130, - 172, - 219, - 225, - 161, - 139, - 229, - 89, + 177, + 2, + 236, + 207, 243, - 125, + 134, + 135, + 210, + 143, + 87, + 232, + 215, + 128, 194, - 213, - 209, - 30, - 23, - 174, - 100, - 244, - 124, - 74, - 140, - 47 + 120, + 113, + 224, + 4, + 165 ], "account": { "program_owner": [ diff --git a/indexer/service/rpc/src/lib.rs b/indexer/service/rpc/src/lib.rs index a769a2a7..2a67ac50 100644 --- a/indexer/service/rpc/src/lib.rs +++ b/indexer/service/rpc/src/lib.rs @@ -42,7 +42,11 @@ pub trait Rpc { async fn get_transaction(&self, tx_hash: HashType) -> Result; #[method(name = "getBlocks")] - async fn get_blocks(&self, offset: u32, limit: u32) -> Result, ErrorObjectOwned>; + async fn get_blocks( + &self, + before: Option, + limit: u32, + ) -> Result, ErrorObjectOwned>; #[method(name = "getTransactionsByAccount")] async fn get_transactions_by_account( diff --git a/indexer/service/src/mock_service.rs b/indexer/service/src/mock_service.rs index 3343e9cc..5f0cfbf2 100644 --- a/indexer/service/src/mock_service.rs +++ b/indexer/service/src/mock_service.rs @@ -43,10 +43,10 @@ impl MockIndexerService { ); } - // Create 10 blocks with transactions + // Create 100 blocks with transactions let mut prev_hash = HashType([0u8; 32]); - for block_id in 0..10 { + for block_id in 1..=100 { let block_hash = { let mut hash = [0u8; 32]; hash[0] = block_id as u8; @@ -225,23 +225,20 @@ impl indexer_service_rpc::RpcServer for MockIndexerService { .ok_or_else(|| ErrorObjectOwned::owned(-32001, "Transaction not found", None::<()>)) } - async fn get_blocks(&self, offset: u32, limit: u32) -> Result, ErrorObjectOwned> { - let offset = offset as usize; - let limit = limit as usize; - let total = self.blocks.len(); + async fn get_blocks( + &self, + before: Option, + limit: u32, + ) -> Result, ErrorObjectOwned> { + let start_id = before.map_or_else(|| self.blocks.len() as u64, |id| id.saturating_sub(1)); - // Return blocks in reverse order (newest first), with pagination - let start = offset.min(total); - let end = (offset + limit).min(total); - - Ok(self - .blocks - .iter() + let result = (1..=start_id) .rev() - .skip(start) - .take(end - start) - .cloned() - .collect()) + .take(limit as usize) + .map_while(|block_id| self.blocks.get(block_id as usize - 1).cloned()) + .collect(); + + Ok(result) } async fn get_transactions_by_account( diff --git a/indexer/service/src/service.rs b/indexer/service/src/service.rs index 13ec2ea8..da3e7cbd 100644 --- a/indexer/service/src/service.rs +++ b/indexer/service/src/service.rs @@ -88,11 +88,15 @@ impl indexer_service_rpc::RpcServer for IndexerService { .into()) } - async fn get_blocks(&self, offset: u32, limit: u32) -> Result, ErrorObjectOwned> { + async fn get_blocks( + &self, + before: Option, + limit: u32, + ) -> Result, ErrorObjectOwned> { let blocks = self .indexer .store - .get_block_batch(offset as u64, limit as u64) + .get_block_batch(before, limit as u64) .map_err(db_error)?; let mut block_res = vec![]; diff --git a/integration_tests/tests/indexer.rs b/integration_tests/tests/indexer.rs index d5207a41..ad169790 100644 --- a/integration_tests/tests/indexer.rs +++ b/integration_tests/tests/indexer.rs @@ -61,8 +61,11 @@ async fn indexer_block_batching() -> Result<()> { assert!(last_block_indexer > 1); - // Getting wide batch to fit all blocks - let block_batch = ctx.indexer_client().get_blocks(1, 100).await.unwrap(); + // Getting wide batch to fit all blocks (from latest backwards) + let mut block_batch = ctx.indexer_client().get_blocks(None, 100).await.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; diff --git a/integration_tests/tests/keys_restoration.rs b/integration_tests/tests/keys_restoration.rs index 24299a56..1bd207be 100644 --- a/integration_tests/tests/keys_restoration.rs +++ b/integration_tests/tests/keys_restoration.rs @@ -1,9 +1,9 @@ use std::{str::FromStr, time::Duration}; -use anyhow::Result; +use anyhow::{Context, Result}; use integration_tests::{ - TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, format_private_account_id, - format_public_account_id, verify_commitment_is_in_state, + TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, fetch_privacy_preserving_tx, + format_private_account_id, format_public_account_id, verify_commitment_is_in_state, }; use key_protocol::key_management::key_tree::chain_index::ChainIndex; use log::info; @@ -15,6 +15,93 @@ use wallet::cli::{ programs::native_token_transfer::AuthTransferSubcommand, }; +#[test] +async fn sync_private_account_with_non_zero_chain_index() -> Result<()> { + let mut ctx = TestContext::new().await?; + + let from: AccountId = ctx.existing_private_accounts()[0]; + + // Create a new private account + let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private { + cci: None, + label: None, + })); + + for _ in 0..3 { + // Key Tree shift + // This way we have account with child index > 0. + let result = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::New(NewSubcommand::Private { + cci: None, + label: None, + })), + ) + .await?; + let SubcommandReturnValue::RegisterAccount { account_id: _ } = result else { + anyhow::bail!("Expected RegisterAccount return value"); + }; + } + + let sub_ret = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + let SubcommandReturnValue::RegisterAccount { + account_id: to_account_id, + } = sub_ret + else { + anyhow::bail!("Expected RegisterAccount return value"); + }; + + // Get the keys for the newly created account + let (to_keys, _) = 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) + let command = Command::AuthTransfer(AuthTransferSubcommand::Send { + from: format_private_account_id(from), + to: None, + to_npk: Some(hex::encode(to_keys.nullifer_public_key.0)), + to_vpk: Some(hex::encode(to_keys.viewing_public_key.0)), + amount: 100, + }); + + let sub_ret = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash } = sub_ret else { + anyhow::bail!("Expected PrivacyPreservingTransfer return value"); + }; + + let tx = fetch_privacy_preserving_tx(ctx.sequencer_client(), tx_hash).await; + + // Sync the wallet to claim the new account + let command = Command::Account(AccountSubcommand::SyncPrivate {}); + wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + + let new_commitment1 = ctx + .wallet() + .get_private_account_commitment(from) + .context("Failed to get private account commitment for sender")?; + assert_eq!(tx.message.new_commitments[0], new_commitment1); + + assert_eq!(tx.message.new_commitments.len(), 2); + for commitment in tx.message.new_commitments.into_iter() { + assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await); + } + + let to_res_acc = ctx + .wallet() + .get_account_private(to_account_id) + .context("Failed to get recipient's private account")?; + assert_eq!(to_res_acc.balance, 100); + + info!("Successfully transferred using claiming path"); + + Ok(()) +} + #[test] async fn restore_keys_from_seed() -> Result<()> { let mut ctx = TestContext::new().await?; diff --git a/integration_tests/tests/pinata.rs b/integration_tests/tests/pinata.rs index 002dd2c7..da5f13c3 100644 --- a/integration_tests/tests/pinata.rs +++ b/integration_tests/tests/pinata.rs @@ -16,6 +16,118 @@ use wallet::cli::{ }, }; +#[test] +async fn claim_pinata_to_uninitialized_public_account_fails_fast() -> Result<()> { + let mut ctx = TestContext::new().await?; + + let result = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::New(NewSubcommand::Public { + cci: None, + label: None, + })), + ) + .await?; + let SubcommandReturnValue::RegisterAccount { + account_id: winner_account_id, + } = result + else { + anyhow::bail!("Expected RegisterAccount return value"); + }; + + let winner_account_id_formatted = format_public_account_id(winner_account_id); + + let pinata_balance_pre = ctx + .sequencer_client() + .get_account_balance(PINATA_BASE58.parse().unwrap()) + .await? + .balance; + + let claim_result = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Pinata(PinataProgramAgnosticSubcommand::Claim { + to: winner_account_id_formatted, + }), + ) + .await; + + assert!( + claim_result.is_err(), + "Expected uninitialized account error" + ); + let err = claim_result.unwrap_err().to_string(); + assert!( + err.contains("wallet auth-transfer init --account-id Public/"), + "Expected init guidance, got: {err}", + ); + + let pinata_balance_post = ctx + .sequencer_client() + .get_account_balance(PINATA_BASE58.parse().unwrap()) + .await? + .balance; + + assert_eq!(pinata_balance_post, pinata_balance_pre); + + Ok(()) +} + +#[test] +async fn claim_pinata_to_uninitialized_private_account_fails_fast() -> Result<()> { + let mut ctx = TestContext::new().await?; + + let result = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::New(NewSubcommand::Private { + cci: None, + label: None, + })), + ) + .await?; + let SubcommandReturnValue::RegisterAccount { + account_id: winner_account_id, + } = result + else { + anyhow::bail!("Expected RegisterAccount return value"); + }; + + let winner_account_id_formatted = format_private_account_id(winner_account_id); + + let pinata_balance_pre = ctx + .sequencer_client() + .get_account_balance(PINATA_BASE58.parse().unwrap()) + .await? + .balance; + + let claim_result = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Pinata(PinataProgramAgnosticSubcommand::Claim { + to: winner_account_id_formatted, + }), + ) + .await; + + assert!( + claim_result.is_err(), + "Expected uninitialized account error" + ); + let err = claim_result.unwrap_err().to_string(); + assert!( + err.contains("wallet auth-transfer init --account-id Private/"), + "Expected init guidance, got: {err}", + ); + + let pinata_balance_post = ctx + .sequencer_client() + .get_account_balance(PINATA_BASE58.parse().unwrap()) + .await? + .balance; + + assert_eq!(pinata_balance_post, pinata_balance_pre); + + Ok(()) +} + #[test] async fn claim_pinata_to_existing_public_account() -> Result<()> { let mut ctx = TestContext::new().await?; diff --git a/key_protocol/src/key_management/key_tree/chain_index.rs b/key_protocol/src/key_management/key_tree/chain_index.rs index 6dbaf9a9..d5fbf401 100644 --- a/key_protocol/src/key_management/key_tree/chain_index.rs +++ b/key_protocol/src/key_management/key_tree/chain_index.rs @@ -68,6 +68,10 @@ impl ChainIndex { &self.0 } + pub fn index(&self) -> Option { + self.chain().last().copied() + } + pub fn next_in_line(&self) -> ChainIndex { let mut chain = self.0.clone(); // ToDo: Add overflow check diff --git a/key_protocol/src/key_management/mod.rs b/key_protocol/src/key_management/mod.rs index d46dcf35..6e2891ce 100644 --- a/key_protocol/src/key_management/mod.rs +++ b/key_protocol/src/key_management/mod.rs @@ -62,9 +62,10 @@ impl KeyChain { pub fn calculate_shared_secret_receiver( &self, ephemeral_public_key_sender: EphemeralPublicKey, + index: Option, ) -> SharedSecretKey { SharedSecretKey::new( - &self.secret_spending_key.generate_viewing_secret_key(None), + &self.secret_spending_key.generate_viewing_secret_key(index), &ephemeral_public_key_sender, ) } @@ -78,6 +79,9 @@ mod tests { use rand::RngCore; use super::*; + use crate::key_management::{ + ephemeral_key_holder::EphemeralKeyHolder, key_tree::KeyTreePrivate, + }; #[test] fn test_new_os_random() { @@ -101,8 +105,8 @@ mod tests { let ephemeral_public_key_sender = EphemeralPublicKey::from_scalar(scalar); // Calculate shared secret - let _shared_secret = - account_id_key_holder.calculate_shared_secret_receiver(ephemeral_public_key_sender); + let _shared_secret = account_id_key_holder + .calculate_shared_secret_receiver(ephemeral_public_key_sender, None); } #[test] @@ -150,4 +154,40 @@ mod tests { hex::encode(viewing_public_key.to_bytes()) ); } + + fn account_with_chain_index_2_for_tests() -> KeyChain { + let seed = SeedHolder::new_os_random(); + let mut key_tree_private = KeyTreePrivate::new(&seed); + + // /0 + key_tree_private.generate_new_node_layered().unwrap(); + // /1 + key_tree_private.generate_new_node_layered().unwrap(); + // /0/0 + key_tree_private.generate_new_node_layered().unwrap(); + // /2 + let (second_child_id, _) = key_tree_private.generate_new_node_layered().unwrap(); + + key_tree_private + .get_node(second_child_id) + .unwrap() + .value + .0 + .clone() + } + + #[test] + fn test_non_trivial_chain_index() { + let keys = account_with_chain_index_2_for_tests(); + + let eph_key_holder = EphemeralKeyHolder::new(&keys.nullifer_public_key); + + let key_sender = eph_key_holder.calculate_shared_secret_sender(&keys.viewing_public_key); + let key_receiver = keys.calculate_shared_secret_receiver( + eph_key_holder.generate_ephemeral_public_key(), + Some(2), + ); + + assert_eq!(key_sender.0, key_receiver.0); + } } diff --git a/nssa/core/src/account.rs b/nssa/core/src/account.rs index 6e368e0e..d6ea3429 100644 --- a/nssa/core/src/account.rs +++ b/nssa/core/src/account.rs @@ -83,7 +83,7 @@ impl BorshDeserialize for Nonce { /// Account to be used both in public and private contexts #[derive( - Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, + Default, Clone, Eq, PartialEq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, )] pub struct Account { pub program_owner: ProgramId, @@ -92,6 +92,23 @@ pub struct Account { pub nonce: Nonce, } +impl std::fmt::Debug for Account { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let program_owner_hex: String = self + .program_owner + .iter() + .flat_map(|n| n.to_le_bytes()) + .map(|b| format!("{b:02x}")) + .collect(); + f.debug_struct("Account") + .field("program_owner", &program_owner_hex) + .field("balance", &self.balance) + .field("data", &self.data) + .field("nonce", &self.nonce) + .finish() + } +} + #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct AccountWithMetadata { pub account: Account, @@ -111,7 +128,6 @@ impl AccountWithMetadata { } #[derive( - Debug, Default, Copy, Clone, @@ -128,6 +144,12 @@ pub struct AccountId { value: [u8; 32], } +impl std::fmt::Debug for AccountId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.value.to_base58()) + } +} + impl AccountId { pub fn new(value: [u8; 32]) -> Self { Self { value } diff --git a/nssa/core/src/commitment.rs b/nssa/core/src/commitment.rs index 9a5bce2d..d15b4df1 100644 --- a/nssa/core/src/commitment.rs +++ b/nssa/core/src/commitment.rs @@ -7,10 +7,18 @@ use crate::{NullifierPublicKey, account::Account}; #[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[cfg_attr( any(feature = "host", test), - derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord) + derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord) )] pub struct Commitment(pub(super) [u8; 32]); +#[cfg(any(feature = "host", test))] +impl std::fmt::Debug for Commitment { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let hex: String = self.0.iter().map(|b| format!("{b:02x}")).collect(); + write!(f, "Commitment({hex})") + } +} + /// A commitment to all zero data. /// ```python /// from hashlib import sha256 diff --git a/nssa/core/src/encryption/mod.rs b/nssa/core/src/encryption/mod.rs index 9ccbf2c8..8c2ff154 100644 --- a/nssa/core/src/encryption/mod.rs +++ b/nssa/core/src/encryption/mod.rs @@ -22,9 +22,17 @@ pub struct SharedSecretKey(pub [u8; 32]); pub struct EncryptionScheme; #[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)] -#[cfg_attr(any(feature = "host", test), derive(Debug, Clone, PartialEq, Eq))] +#[cfg_attr(any(feature = "host", test), derive(Clone, PartialEq, Eq))] pub struct Ciphertext(pub(crate) Vec); +#[cfg(any(feature = "host", test))] +impl std::fmt::Debug for Ciphertext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let hex: String = self.0.iter().map(|b| format!("{b:02x}")).collect(); + write!(f, "Ciphertext({hex})") + } +} + impl EncryptionScheme { pub fn encrypt( account: &Account, @@ -75,6 +83,17 @@ impl EncryptionScheme { Self::symmetric_transform(&mut buffer, shared_secret, commitment, output_index); let mut cursor = Cursor::new(buffer.as_slice()); - Account::from_cursor(&mut cursor).ok() + Account::from_cursor(&mut cursor) + .inspect_err(|err| { + println!( + "Failed to decode {ciphertext:?} \n + with secret {:?} ,\n + commitment {commitment:?} ,\n + and output_index {output_index} ,\n + with error {err:?}", + shared_secret.0 + ) + }) + .ok() } } diff --git a/nssa/core/src/encryption/shared_key_derivation.rs b/nssa/core/src/encryption/shared_key_derivation.rs index e946d5e3..6a6636b2 100644 --- a/nssa/core/src/encryption/shared_key_derivation.rs +++ b/nssa/core/src/encryption/shared_key_derivation.rs @@ -10,9 +10,16 @@ use serde::{Deserialize, Serialize}; use crate::{SharedSecretKey, encryption::Scalar}; -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct Secp256k1Point(pub Vec); +impl std::fmt::Debug for Secp256k1Point { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let hex: String = self.0.iter().map(|b| format!("{b:02x}")).collect(); + write!(f, "Secp256k1Point({hex})") + } +} + impl Secp256k1Point { pub fn from_scalar(value: Scalar) -> Secp256k1Point { let x_bytes: FieldBytes = value.into(); diff --git a/nssa/core/src/nullifier.rs b/nssa/core/src/nullifier.rs index c019b185..95006600 100644 --- a/nssa/core/src/nullifier.rs +++ b/nssa/core/src/nullifier.rs @@ -45,10 +45,18 @@ pub type NullifierSecretKey = [u8; 32]; #[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[cfg_attr( any(feature = "host", test), - derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash) + derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash) )] pub struct Nullifier(pub(super) [u8; 32]); +#[cfg(any(feature = "host", test))] +impl std::fmt::Debug for Nullifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let hex: String = self.0.iter().map(|b| format!("{b:02x}")).collect(); + write!(f, "Nullifier({hex})") + } +} + impl Nullifier { pub fn for_account_update(commitment: &Commitment, nsk: &NullifierSecretKey) -> Self { const UPDATE_PREFIX: &[u8; 32] = b"/NSSA/v0.2/Nullifier/Update/\x00\x00\x00\x00"; diff --git a/nssa/src/error.rs b/nssa/src/error.rs index 45d53108..c42b70d2 100644 --- a/nssa/src/error.rs +++ b/nssa/src/error.rs @@ -2,6 +2,15 @@ use std::io; use thiserror::Error; +#[macro_export] +macro_rules! ensure { + ($cond:expr, $err:expr) => { + if !$cond { + return Err($err); + } + }; +} + #[derive(Error, Debug)] pub enum NssaError { #[error("Invalid input: {0}")] @@ -58,3 +67,24 @@ pub enum NssaError { #[error("Chain of calls is too long")] MaxChainedCallsDepthExceeded, } + +#[cfg(test)] +mod tests { + + #[derive(Debug)] + enum TestError { + TestErr, + } + + fn test_function_ensure(cond: bool) -> Result<(), TestError> { + ensure!(cond, TestError::TestErr); + + Ok(()) + } + + #[test] + fn test_ensure() { + assert!(test_function_ensure(true).is_ok()); + assert!(test_function_ensure(false).is_err()); + } +} diff --git a/nssa/src/privacy_preserving_transaction/message.rs b/nssa/src/privacy_preserving_transaction/message.rs index 02796855..45bbe245 100644 --- a/nssa/src/privacy_preserving_transaction/message.rs +++ b/nssa/src/privacy_preserving_transaction/message.rs @@ -43,7 +43,7 @@ impl EncryptedAccountData { } } -#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +#[derive(Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct Message { pub public_account_ids: Vec, pub nonces: Vec, @@ -53,6 +53,33 @@ pub struct Message { pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>, } +impl std::fmt::Debug for Message { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + struct HexDigest<'a>(&'a [u8; 32]); + impl std::fmt::Debug for HexDigest<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", hex::encode(self.0)) + } + } + let nullifiers: Vec<_> = self + .new_nullifiers + .iter() + .map(|(n, d)| (n, HexDigest(d))) + .collect(); + f.debug_struct("Message") + .field("public_account_ids", &self.public_account_ids) + .field("nonces", &self.nonces) + .field("public_post_states", &self.public_post_states) + .field( + "encrypted_private_post_states", + &self.encrypted_private_post_states, + ) + .field("new_commitments", &self.new_commitments) + .field("new_nullifiers", &nullifiers) + .finish() + } +} + impl Message { pub fn try_from_circuit_output( public_account_ids: Vec, diff --git a/nssa/src/public_transaction/message.rs b/nssa/src/public_transaction/message.rs index 36a20fbb..776151cf 100644 --- a/nssa/src/public_transaction/message.rs +++ b/nssa/src/public_transaction/message.rs @@ -7,7 +7,7 @@ use serde::Serialize; use crate::{AccountId, error::NssaError, program::Program}; -#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +#[derive(Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct Message { pub program_id: ProgramId, pub account_ids: Vec, @@ -15,6 +15,23 @@ pub struct Message { pub instruction_data: InstructionData, } +impl std::fmt::Debug for Message { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let program_id_hex = hex::encode( + self.program_id + .iter() + .flat_map(|n| n.to_le_bytes()) + .collect::>(), + ); + f.debug_struct("Message") + .field("program_id", &program_id_hex) + .field("account_ids", &self.account_ids) + .field("nonces", &self.nonces) + .field("instruction_data", &self.instruction_data) + .finish() + } +} + impl Message { pub fn try_new( program_id: ProgramId, diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index 60048938..b4aa6b63 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -9,7 +9,7 @@ use nssa_core::{ use sha2::{Digest, digest::FixedOutput}; use crate::{ - V02State, + V02State, ensure, error::NssaError, public_transaction::{Message, WitnessSet}, state::MAX_NUMBER_CHAINED_CALLS, @@ -70,33 +70,33 @@ impl PublicTransaction { let witness_set = self.witness_set(); // All account_ids must be different - if message.account_ids.iter().collect::>().len() != message.account_ids.len() { - return Err(NssaError::InvalidInput( - "Duplicate account_ids found in message".into(), - )); - } + ensure!( + message.account_ids.iter().collect::>().len() == message.account_ids.len(), + NssaError::InvalidInput("Duplicate account_ids found in message".into(),) + ); // Check exactly one nonce is provided for each signature - if message.nonces.len() != witness_set.signatures_and_public_keys.len() { - return Err(NssaError::InvalidInput( + ensure!( + message.nonces.len() == witness_set.signatures_and_public_keys.len(), + NssaError::InvalidInput( "Mismatch between number of nonces and signatures/public keys".into(), - )); - } + ) + ); // Check the signatures are valid - if !witness_set.is_valid_for(message) { - return Err(NssaError::InvalidInput( - "Invalid signature for given message and public key".into(), - )); - } + ensure!( + witness_set.is_valid_for(message), + NssaError::InvalidInput("Invalid signature for given message and public key".into()) + ); let signer_account_ids = self.signer_account_ids(); // Check nonces corresponds to the current nonces on the public state. for (account_id, nonce) in signer_account_ids.iter().zip(&message.nonces) { let current_nonce = state.get_account_by_id(*account_id).nonce; - if current_nonce != *nonce { - return Err(NssaError::InvalidInput("Nonce mismatch".into())); - } + ensure!( + current_nonce == *nonce, + NssaError::InvalidInput("Nonce mismatch".into()) + ); } // Build pre_states for execution @@ -125,9 +125,10 @@ impl PublicTransaction { let mut chain_calls_counter = 0; while let Some((chained_call, caller_program_id)) = chained_calls.pop_front() { - if chain_calls_counter > MAX_NUMBER_CHAINED_CALLS { - return Err(NssaError::MaxChainedCallsDepthExceeded); - } + ensure!( + chain_calls_counter <= MAX_NUMBER_CHAINED_CALLS, + NssaError::MaxChainedCallsDepthExceeded + ); // Check that the `program_id` corresponds to a deployed program let Some(program) = state.programs().get(&chained_call.program_id) else { @@ -158,28 +159,31 @@ impl PublicTransaction { .get(&account_id) .cloned() .unwrap_or_else(|| state.get_account_by_id(account_id)); - if pre.account != expected_pre { - return Err(NssaError::InvalidProgramBehavior); - } + ensure!( + pre.account == expected_pre, + NssaError::InvalidProgramBehavior + ); // Check that authorization flags are consistent with the provided ones or // authorized by program through the PDA mechanism let is_authorized = signer_account_ids.contains(&account_id) || authorized_pdas.contains(&account_id); - if pre.is_authorized != is_authorized { - return Err(NssaError::InvalidProgramBehavior); - } + ensure!( + pre.is_authorized == is_authorized, + NssaError::InvalidProgramBehavior + ); } // Verify execution corresponds to a well-behaved program. // See the # Programs section for the definition of the `validate_execution` method. - if !validate_execution( - &program_output.pre_states, - &program_output.post_states, - chained_call.program_id, - ) { - return Err(NssaError::InvalidProgramBehavior); - } + ensure!( + validate_execution( + &program_output.pre_states, + &program_output.post_states, + chained_call.program_id, + ), + NssaError::InvalidProgramBehavior + ); for post in program_output .post_states @@ -221,9 +225,10 @@ impl PublicTransaction { } Some(post) }) { - if post.program_owner == DEFAULT_PROGRAM_ID { - return Err(NssaError::InvalidProgramBehavior); - } + ensure!( + post.program_owner != DEFAULT_PROGRAM_ID, + NssaError::InvalidProgramBehavior + ); } Ok(state_diff) diff --git a/nssa/src/signature/mod.rs b/nssa/src/signature/mod.rs index f76c480a..24094c5d 100644 --- a/nssa/src/signature/mod.rs +++ b/nssa/src/signature/mod.rs @@ -6,11 +6,17 @@ pub use private_key::PrivateKey; pub use public_key::PublicKey; use rand::{RngCore, rngs::OsRng}; -#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +#[derive(Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct Signature { pub value: [u8; 64], } +impl std::fmt::Debug for Signature { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", hex::encode(self.value)) + } +} + impl Signature { pub fn new(key: &PrivateKey, message: &[u8]) -> Self { let mut aux_random = [0u8; 32]; diff --git a/nssa/src/signature/public_key.rs b/nssa/src/signature/public_key.rs index 55e55b57..83fbf0cb 100644 --- a/nssa/src/signature/public_key.rs +++ b/nssa/src/signature/public_key.rs @@ -5,9 +5,15 @@ use sha2::{Digest, Sha256}; use crate::{PrivateKey, error::NssaError}; -#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, Serialize, Deserialize)] +#[derive(Clone, PartialEq, Eq, BorshSerialize, Serialize, Deserialize)] pub struct PublicKey([u8; 32]); +impl std::fmt::Debug for PublicKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", hex::encode(self.0)) + } +} + impl BorshDeserialize for PublicKey { fn deserialize_reader(reader: &mut R) -> std::io::Result { let mut buf = [0u8; 32]; diff --git a/nssa/src/state.rs b/nssa/src/state.rs index aa2852b5..eeab2fab 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -284,7 +284,7 @@ impl V02State { account_id, Account { program_owner: Program::pinata().id(), - balance: 1500, + balance: 1500000, // Difficulty: 3 data: vec![3; 33].try_into().expect("should fit"), nonce: 0u128.into(), diff --git a/sequencer_runner/configs/debug/sequencer_config.json b/sequencer_runner/configs/debug/sequencer_config.json index b53124c2..2313ae20 100644 --- a/sequencer_runner/configs/debug/sequencer_config.json +++ b/sequencer_runner/configs/debug/sequencer_config.json @@ -1,5 +1,5 @@ { - "home": "./sequencer_runner", + "home": ".", "override_rust_log": null, "genesis_id": 1, "is_genesis_random": true, diff --git a/sequencer_runner/src/lib.rs b/sequencer_runner/src/lib.rs index d74792c8..944a6402 100644 --- a/sequencer_runner/src/lib.rs +++ b/sequencer_runner/src/lib.rs @@ -173,7 +173,7 @@ async fn retry_pending_blocks(seq_core: &Arc>) -> Result<() use log::debug; - let (pending_blocks, block_settlement_client) = { + let (mut pending_blocks, block_settlement_client) = { let sequencer_core = seq_core.lock().await; let client = sequencer_core.block_settlement_client(); let pending_blocks = sequencer_core @@ -182,6 +182,8 @@ async fn retry_pending_blocks(seq_core: &Arc>) -> Result<() (pending_blocks, client) }; + pending_blocks.sort_by(|block1, block2| block1.header.block_id.cmp(&block2.header.block_id)); + if !pending_blocks.is_empty() { info!( "Resubmitting blocks from {} to {}", diff --git a/storage/src/indexer.rs b/storage/src/indexer.rs index 94a1a1a0..2c37ab0f 100644 --- a/storage/src/indexer.rs +++ b/storage/src/indexer.rs @@ -539,13 +539,26 @@ impl RocksDBIO { } } - pub fn get_block_batch(&self, offset: u64, limit: u64) -> DbResult> { + pub fn get_block_batch(&self, before: Option, limit: u64) -> DbResult> { let cf_block = self.block_column(); let mut block_batch = vec![]; + // Determine the starting block ID + let start_block_id = if let Some(before_id) = before { + before_id.saturating_sub(1) + } else { + // Get the latest block ID + self.get_meta_last_block_in_db()? + }; + // ToDo: Multi get this - for block_id in offset..(offset + limit) { + for i in 0..limit { + let block_id = start_block_id.saturating_sub(i); + if block_id == 0 { + break; + } + let res = self .db .get_cf( @@ -1215,7 +1228,10 @@ mod tests { let block_hashes_mem: Vec<[u8; 32]> = block_res.into_iter().map(|bl| bl.header.hash.0).collect(); - let batch_res = dbio.get_block_batch(2, 4).unwrap(); + // Get blocks before ID 6 (i.e., starting from 5 going backwards), limit 4 + // This should return blocks 5, 4, 3, 2 in descending order + let mut batch_res = dbio.get_block_batch(Some(6), 4).unwrap(); + batch_res.reverse(); // Reverse to match ascending order for comparison let block_hashes_db: Vec<[u8; 32]> = batch_res.into_iter().map(|bl| bl.header.hash.0).collect(); @@ -1224,7 +1240,10 @@ mod tests { let block_hashes_mem_limited = &block_hashes_mem[1..]; - let batch_res_limited = dbio.get_block_batch(3, 4).unwrap(); + // Get blocks before ID 6, limit 3 + // This should return blocks 5, 4, 3 in descending order + let mut batch_res_limited = dbio.get_block_batch(Some(6), 3).unwrap(); + batch_res_limited.reverse(); // Reverse to match ascending order for comparison let block_hashes_db_limited: Vec<[u8; 32]> = batch_res_limited .into_iter() diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index e43e742c..835b152d 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -21,8 +21,8 @@ humantime-serde.workspace = true humantime.workspace = true tokio = { workspace = true, features = ["macros"] } clap.workspace = true +base58.workspace = true base64.workspace = true -bytemuck.workspace = true borsh.workspace = true hex.workspace = true itertools.workspace = true diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index 7f936284..387a8c1c 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -165,10 +165,13 @@ fn format_account_details(account: &Account) -> (String, String) { let token_prog_id = Program::token().id(); match &account.program_owner { - o if *o == auth_tr_prog_id => ( - "Account owned by authenticated transfer program".to_string(), - serde_json::to_string(&account).unwrap(), - ), + o if *o == auth_tr_prog_id => { + let account_hr: HumanReadableAccount = account.clone().into(); + ( + "Account owned by authenticated transfer program".to_string(), + serde_json::to_string(&account_hr).unwrap(), + ) + } o if *o == token_prog_id => { if let Ok(token_def) = TokenDefinition::try_from(&account.data) { ( diff --git a/wallet/src/cli/programs/pinata.rs b/wallet/src/cli/programs/pinata.rs index b117c4c1..d7e28528 100644 --- a/wallet/src/cli/programs/pinata.rs +++ b/wallet/src/cli/programs/pinata.rs @@ -1,6 +1,7 @@ use anyhow::{Context, Result}; use clap::Subcommand; use common::{PINATA_BASE58, transaction::NSSATransaction}; +use nssa::{Account, AccountId}; use crate::{ AccDecodeData::Decode, @@ -102,17 +103,17 @@ impl WalletSubcommand for PinataProgramSubcommandPublic { pinata_account_id, winner_account_id, } => { - let pinata_account_id = pinata_account_id.parse().unwrap(); + let pinata_account_id = pinata_account_id.parse()?; + let winner_account_id: AccountId = winner_account_id.parse()?; + + ensure_public_recipient_initialized(wallet_core, winner_account_id).await?; + let solution = find_solution(wallet_core, pinata_account_id) .await .context("failed to compute solution")?; let res = Pinata(wallet_core) - .claim( - pinata_account_id, - winner_account_id.parse().unwrap(), - solution, - ) + .claim(pinata_account_id, winner_account_id, solution) .await?; println!("Results of tx send are {res:#?}"); @@ -138,8 +139,11 @@ impl WalletSubcommand for PinataProgramSubcommandPrivate { pinata_account_id, winner_account_id, } => { - let pinata_account_id = pinata_account_id.parse().unwrap(); - let winner_account_id = winner_account_id.parse().unwrap(); + let pinata_account_id = pinata_account_id.parse()?; + let winner_account_id: AccountId = winner_account_id.parse()?; + + ensure_private_owned_recipient_initialized(wallet_core, winner_account_id)?; + let solution = find_solution(wallet_core, pinata_account_id) .await .context("failed to compute solution")?; @@ -188,7 +192,51 @@ impl WalletSubcommand for PinataProgramSubcommand { } } -async fn find_solution(wallet: &WalletCore, pinata_account_id: nssa::AccountId) -> Result { +async fn ensure_public_recipient_initialized( + wallet_core: &WalletCore, + winner_account_id: AccountId, +) -> Result<()> { + let account = wallet_core + .get_account_public(winner_account_id) + .await + .with_context(|| format!("failed to fetch recipient account Public/{winner_account_id}"))?; + + if account == Account::default() { + anyhow::bail!( + "Recipient account Public/{winner_account_id} is uninitialized.\n\ + Initialize it first:\n \ + wallet auth-transfer init --account-id Public/{winner_account_id}" + ); + } + + Ok(()) +} + +fn ensure_private_owned_recipient_initialized( + wallet_core: &WalletCore, + winner_account_id: AccountId, +) -> Result<()> { + let Some(account) = wallet_core.get_account_private(winner_account_id) else { + anyhow::bail!( + "Recipient account Private/{winner_account_id} is not found in this wallet.\n\ + `wallet pinata claim --to Private/...` supports owned private accounts only." + ); + }; + + if account == Account::default() { + anyhow::bail!( + "Recipient account Private/{winner_account_id} is uninitialized.\n\ + Initialize it first:\n \ + wallet auth-transfer init --account-id Private/{winner_account_id}\n\ + Then sync private state:\n \ + wallet account sync-private" + ); + } + + Ok(()) +} + +async fn find_solution(wallet: &WalletCore, pinata_account_id: AccountId) -> Result { let account = wallet.get_account_public(pinata_account_id).await?; let data: [u8; 33] = account .data diff --git a/wallet/src/helperfunctions.rs b/wallet/src/helperfunctions.rs index eda0bb2f..e8e5c9da 100644 --- a/wallet/src/helperfunctions.rs +++ b/wallet/src/helperfunctions.rs @@ -1,7 +1,7 @@ use std::{collections::HashMap, path::PathBuf, str::FromStr}; use anyhow::Result; -use base64::{Engine, engine::general_purpose::STANDARD as BASE64}; +use base58::ToBase58; use key_protocol::key_protocol_core::NSSAUserData; use nssa::Account; use serde::Serialize; @@ -141,15 +141,20 @@ pub(crate) fn parse_addr_with_privacy_prefix( #[derive(Serialize)] pub(crate) struct HumanReadableAccount { balance: u128, - program_owner_b64: String, - data_b64: String, + program_owner: String, + data: String, nonce: u128, } impl From for HumanReadableAccount { fn from(account: Account) -> Self { - let program_owner_b64 = BASE64.encode(bytemuck::cast_slice(&account.program_owner)); - let data_b64 = BASE64.encode(account.data); + let program_owner = account + .program_owner + .iter() + .flat_map(|n| n.to_le_bytes()) + .collect::>() + .to_base58(); + let data = hex::encode(account.data); Self { balance: account.balance, program_owner_b64, diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 5e59abac..d9e25288 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -362,7 +362,7 @@ impl WalletCore { ); let tx = PrivacyPreservingTransaction::new(message, witness_set); - let shared_secrets = private_account_keys + let shared_secrets: Vec<_> = private_account_keys .into_iter() .map(|keys| keys.ssk) .collect(); @@ -418,18 +418,19 @@ impl WalletCore { .user_data .default_user_private_accounts .iter() - .map(|(acc_account_id, (key_chain, _))| (*acc_account_id, key_chain)) - .chain( - self.storage - .user_data - .private_key_tree - .key_map - .values() - .map(|keys_node| (keys_node.account_id(), &keys_node.value.0)), - ); + .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(), + ) + }, + )); let affected_accounts = private_account_key_chains - .flat_map(|(acc_account_id, key_chain)| { + .flat_map(|(acc_account_id, key_chain, index)| { let view_tag = EncryptedAccountData::compute_view_tag( key_chain.nullifer_public_key.clone(), key_chain.viewing_public_key.clone(), @@ -443,8 +444,8 @@ impl WalletCore { .filter_map(|(ciph_id, encrypted_data)| { let ciphertext = &encrypted_data.ciphertext; let commitment = &tx.message.new_commitments[ciph_id]; - let shared_secret = - key_chain.calculate_shared_secret_receiver(encrypted_data.epk.clone()); + let shared_secret = key_chain + .calculate_shared_secret_receiver(encrypted_data.epk.clone(), index); nssa_core::EncryptionScheme::decrypt( ciphertext, @@ -454,6 +455,7 @@ impl WalletCore { ) }) .map(move |res_acc| (acc_account_id, res_acc)) + .collect::>() }) .collect::>();