diff --git a/.deny.toml b/.deny.toml index 57b5f759..fb1ce3cf 100644 --- a/.deny.toml +++ b/.deny.toml @@ -14,8 +14,8 @@ ignore = [ { id = "RUSTSEC-2025-0141", reason = "`bincode` is unmaintained but continuing to use it." }, { id = "RUSTSEC-2023-0089", reason = "atomic-polyfill is pulled transitively via risc0-zkvm; waiting on upstream fix (see https://github.com/risc0/risc0/issues/3453)" }, { id = "RUSTSEC-2026-0097", reason = "`rand` v0.8.5 is present transitively from logos crates, modification may break integration" }, - { id = "RUSTSEC-2026-0118", reason = "`hickory-proto` v0.25.0-alpha.5 is present transitively from logos crates, modification may break integration"}, - { id = "RUSTSEC-2026-0119", reason = "`hickory-proto` v0.25.0-alpha.5 is present transitively from logos crates, modification may break integration"}, + { id = "RUSTSEC-2026-0118", reason = "`hickory-proto` v0.25.0-alpha.5 is present transitively from logos crates, modification may break integration" }, + { id = "RUSTSEC-2026-0119", reason = "`hickory-proto` v0.25.0-alpha.5 is present transitively from logos crates, modification may break integration" }, ] yanked = "deny" unused-ignored-advisory = "deny" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f10532a8..d879f72b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -158,35 +158,7 @@ jobs: env: RISC0_DEV_MODE: "1" RUST_LOG: "info" - run: cargo nextest run -p integration_tests -- --skip tps_test --skip indexer - - integration-tests-indexer: - runs-on: ubuntu-latest - timeout-minutes: 60 - steps: - - uses: actions/checkout@v5 - with: - ref: ${{ github.event.pull_request.head.sha || github.head_ref }} - - - uses: ./.github/actions/install-system-deps - - - uses: ./.github/actions/install-risc0 - - - uses: ./.github/actions/install-logos-blockchain-circuits - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Install active toolchain - run: rustup install - - - name: Install nextest - run: cargo install --locked cargo-nextest - - - name: Run tests - env: - RISC0_DEV_MODE: "1" - RUST_LOG: "info" - run: cargo nextest run -p integration_tests indexer -- --skip tps_test + run: cargo nextest run -p integration_tests -- --skip tps_test valid-proof-test: runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index 1c49d7e7..64248df7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -912,6 +912,13 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "authenticated_transfer_core" +version = "0.1.0" +dependencies = [ + "serde", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -1622,6 +1629,7 @@ name = "common" version = "0.1.0" dependencies = [ "anyhow", + "authenticated_transfer_core", "base64 0.22.1", "borsh", "clock_core", @@ -1765,6 +1773,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "convert_case" version = "0.11.0" @@ -2181,6 +2198,7 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ + "convert_case 0.10.0", "proc-macro2", "quote", "rustc_version", @@ -3775,6 +3793,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-stream", + "authenticated_transfer_core", "borsh", "common", "futures", @@ -3915,6 +3934,7 @@ version = "0.1.0" dependencies = [ "anyhow", "ata_core", + "authenticated_transfer_core", "bytesize", "common", "env_logger", @@ -6298,6 +6318,7 @@ name = "nssa" version = "0.1.0" dependencies = [ "anyhow", + "authenticated_transfer_core", "borsh", "clock_core", "env_logger", @@ -7034,6 +7055,7 @@ dependencies = [ "amm_program", "ata_core", "ata_program", + "authenticated_transfer_core", "clock_core", "nssa_core", "risc0-zkvm", @@ -8394,6 +8416,7 @@ name = "sequencer_core" version = "0.1.0" dependencies = [ "anyhow", + "authenticated_transfer_core", "borsh", "bytesize", "chrono", @@ -8408,6 +8431,7 @@ dependencies = [ "nssa", "nssa_core", "rand 0.8.5", + "rocksdb", "serde", "serde_json", "storage", @@ -9143,6 +9167,7 @@ dependencies = [ name = "test_programs" version = "0.1.0" dependencies = [ + "authenticated_transfer_core", "clock_core", "nssa_core", "risc0-zkvm", @@ -10082,10 +10107,13 @@ dependencies = [ "anyhow", "async-stream", "ata_core", + "authenticated_transfer_core", "base58", + "bincode", "bip39", "clap", "common", + "derive_more", "env_logger", "futures", "hex", @@ -10103,6 +10131,7 @@ dependencies = [ "serde", "serde_json", "sha2", + "tempfile", "testnet_initial_state", "thiserror 2.0.18", "token_core", @@ -10115,9 +10144,11 @@ name = "wallet-ffi" version = "0.1.0" dependencies = [ "cbindgen", + "key_protocol", "nssa", "nssa_core", "sequencer_service_rpc", + "serde_json", "tempfile", "tokio", "wallet", diff --git a/Cargo.toml b/Cargo.toml index 1bce967f..be6c2583 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "programs/token", "programs/associated_token_account/core", "programs/associated_token_account", + "programs/authenticated_transfer/core", "sequencer/core", "sequencer/service", "sequencer/service/protocol", @@ -65,6 +66,7 @@ amm_core = { path = "programs/amm/core" } amm_program = { path = "programs/amm" } ata_core = { path = "programs/associated_token_account/core" } ata_program = { path = "programs/associated_token_account" } +authenticated_transfer_core = { path = "programs/authenticated_transfer/core" } test_program_methods = { path = "test_program_methods" } testnet_initial_state = { path = "testnet_initial_state" } @@ -78,6 +80,7 @@ tokio-util = "0.7.18" risc0-zkvm = { version = "3.0.5", features = ['std'] } risc0-build = "3.0.5" anyhow = "1.0.98" +derive_more = "2.1.1" num_cpus = "1.13.1" openssl = { version = "0.10", features = ["vendored"] } openssl-probe = { version = "0.1.2" } diff --git a/artifacts/program_methods/amm.bin b/artifacts/program_methods/amm.bin index a7ddba52..9b9bbf9a 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 b0e5def5..55e7c94d 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 f6d9672c..48c065ba 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 c24f463c..27096692 100644 Binary files a/artifacts/program_methods/clock.bin and b/artifacts/program_methods/clock.bin differ diff --git a/artifacts/program_methods/genesis_supply_account.bin b/artifacts/program_methods/genesis_supply_account.bin new file mode 100644 index 00000000..c377a1e6 Binary files /dev/null and b/artifacts/program_methods/genesis_supply_account.bin differ diff --git a/artifacts/program_methods/genesis_supply_private_account.bin b/artifacts/program_methods/genesis_supply_private_account.bin new file mode 100644 index 00000000..9d6aa313 Binary files /dev/null and b/artifacts/program_methods/genesis_supply_private_account.bin differ diff --git a/artifacts/program_methods/pinata.bin b/artifacts/program_methods/pinata.bin index 415c8ce3..ef1edefe 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 9a292cbe..5c65366f 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 3580ef74..e6772240 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 bf0f0571..89015992 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 7292d329..eee2f222 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/auth_transfer_proxy.bin b/artifacts/test_program_methods/auth_transfer_proxy.bin index 662f2d06..76c4b953 100644 Binary files a/artifacts/test_program_methods/auth_transfer_proxy.bin and b/artifacts/test_program_methods/auth_transfer_proxy.bin differ diff --git a/artifacts/test_program_methods/burner.bin b/artifacts/test_program_methods/burner.bin index 30fdcaee..75b9ca0e 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 68edc95c..bf944906 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 5a71455c..970d9cf4 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 42ca125b..be5cc6b1 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 3e84cd25..589131f7 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 705a1ec5..acaf3233 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 9f077174..8915c64b 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 ec26c2ca..91be3cfb 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 73b3bb32..4280c7e8 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 dba3f365..03669255 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 4762d25d..f02d1655 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 653ece66..d57676ab 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 a0144fce..21d1a84a 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 cbf3e467..5fc04f94 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 e2b7fb47..94fb2835 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 a99aa6ae..48582a69 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 d7bffd5f..82c85571 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 b1dfd47f..e2d2da70 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 eaad2613..b2d1d68c 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 c1caf235..3e0a1025 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 2e22bfaa..8568b9b1 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 1f744230..7c7fb0c9 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 90723ae0..4af11d59 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 05c17133..49b55dc4 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 fd6423fc..d4ed3e9d 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 0c86a460..2531a59d 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/Cargo.toml b/common/Cargo.toml index dbf5ec0c..5d8e278c 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -10,6 +10,7 @@ workspace = true [dependencies] nssa.workspace = true nssa_core.workspace = true +authenticated_transfer_core.workspace = true clock_core.workspace = true anyhow.workspace = true diff --git a/common/src/test_utils.rs b/common/src/test_utils.rs index 267d10ce..806048e1 100644 --- a/common/src/test_utils.rs +++ b/common/src/test_utils.rs @@ -47,12 +47,11 @@ pub fn produce_dummy_empty_transaction() -> NSSATransaction { let program_id = nssa::program::Program::authenticated_transfer_program().id(); let account_ids = vec![]; let nonces = vec![]; - let instruction_data: u128 = 0; let message = nssa::public_transaction::Message::try_new( program_id, account_ids, nonces, - instruction_data, + authenticated_transfer_core::Instruction::Initialize, ) .unwrap(); let private_key = nssa::PrivateKey::try_new([1; 32]).unwrap(); @@ -78,7 +77,9 @@ pub fn create_transaction_native_token_transfer( program_id, account_ids, nonces, - balance_to_move, + authenticated_transfer_core::Instruction::Transfer { + amount: balance_to_move, + }, ) .unwrap(); let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]); diff --git a/configs/docker-all-in-one/indexer_config.json b/configs/docker-all-in-one/indexer_config.json index ca99a90c..f2005ff5 100644 --- a/configs/docker-all-in-one/indexer_config.json +++ b/configs/docker-all-in-one/indexer_config.json @@ -4,153 +4,5 @@ "bedrock_config": { "addr": "http://logos-blockchain-node-0:18080" }, - "channel_id": "0101010101010101010101010101010101010101010101010101010101010101", - "initial_accounts": [ - { - "account_id": "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV", - "balance": 10000 - }, - { - "account_id": "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo", - "balance": 20000 - } - ], - "initial_commitments": [ - { - "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, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - "balance": 10000, - "data": [], - "nonce": 0 - } - }, - { - "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, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - "balance": 20000, - "data": [], - "nonce": 0 - } - } - ], - "signing_key": [ - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37 - ] + "channel_id": "0101010101010101010101010101010101010101010101010101010101010101" } diff --git a/configs/docker-all-in-one/sequencer_config.json b/configs/docker-all-in-one/sequencer_config.json index d7fd3490..36eee2bd 100644 --- a/configs/docker-all-in-one/sequencer_config.json +++ b/configs/docker-all-in-one/sequencer_config.json @@ -16,117 +16,29 @@ "node_url": "http://logos-blockchain-node-0:18080" }, "indexer_rpc_url": "ws://indexer_service:8779", - "initial_accounts": [ + "genesis": [ { - "account_id": "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV", - "balance": 10000 - }, - { - "account_id": "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo", - "balance": 20000 - } - ], - "initial_commitments": [ - { - "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, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - "balance": 10000, - "data": [], - "nonce": 0 + "supply_public_account": { + "account_id": "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV", + "balance": 10000 } }, { - "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, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - "balance": 20000, - "data": [], - "nonce": 0 + "supply_public_account": { + "account_id": "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo", + "balance": 20000 + } + }, + { + "supply_private_account": { + "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], + "balance": 10000 + } + }, + { + "supply_private_account": { + "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], + "balance": 20000 } } ], diff --git a/examples/program_deployment/src/bin/run_hello_world_with_authorization.rs b/examples/program_deployment/src/bin/run_hello_world_with_authorization.rs index a9750bce..18b4ba80 100644 --- a/examples/program_deployment/src/bin/run_hello_world_with_authorization.rs +++ b/examples/program_deployment/src/bin/run_hello_world_with_authorization.rs @@ -50,8 +50,8 @@ async fn main() { // Load signing keys to provide authorization let signing_key = wallet_core .storage() - .user_data - .get_pub_account_signing_key(account_id) + .key_chain() + .pub_account_signing_key(account_id) .expect("Input account should be a self owned public account"); // Define the desired greeting in ASCII diff --git a/explorer_service/src/api.rs b/explorer_service/src/api.rs index 8c2a0e36..5984a636 100644 --- a/explorer_service/src/api.rs +++ b/explorer_service/src/api.rs @@ -86,7 +86,7 @@ pub async fn get_block_by_id(block_id: BlockId) -> Result /// Get latest block ID #[server] -pub async fn get_latest_block_id() -> Result { +pub async fn get_latest_block_id() -> Result, ServerFnError> { use indexer_service_rpc::RpcClient as _; let client = expect_context::(); client diff --git a/indexer/core/Cargo.toml b/indexer/core/Cargo.toml index d609f5cb..6c7ad01f 100644 --- a/indexer/core/Cargo.toml +++ b/indexer/core/Cargo.toml @@ -29,3 +29,4 @@ tokio.workspace = true [dev-dependencies] tempfile.workspace = true +authenticated_transfer_core.workspace = true diff --git a/indexer/core/src/block_store.rs b/indexer/core/src/block_store.rs index bfba5ed2..3b593071 100644 --- a/indexer/core/src/block_store.rs +++ b/indexer/core/src/block_store.rs @@ -5,9 +5,10 @@ use common::{ block::{BedrockStatus, Block}, transaction::{NSSATransaction, clock_invocation}, }; +use log::info; use logos_blockchain_core::{header::HeaderId, mantle::ops::channel::MsgId}; use logos_blockchain_zone_sdk::Slot; -use nssa::{Account, AccountId, V03State}; +use nssa::{Account, AccountId, V03State, ValidatedStateDiff}; use nssa_core::BlockId; use storage::indexer::RocksDBIO; use tokio::sync::RwLock; @@ -21,14 +22,10 @@ pub struct IndexerStore { impl IndexerStore { /// Starting database at the start of new chain. /// Creates files if necessary. - /// - /// ATTENTION: Will overwrite genesis block. - pub fn open_db_with_genesis( - location: &Path, - genesis_block: &Block, - initial_state: &V03State, - ) -> Result { - let dbio = RocksDBIO::open_or_create(location, genesis_block, initial_state)?; + pub fn open_db(location: &Path) -> Result { + let initial_state = testnet_initial_state::initial_state(); + let dbio = RocksDBIO::open_or_create(location, &initial_state)?; + let current_state = dbio.final_state()?; Ok(Self { @@ -44,8 +41,8 @@ impl IndexerStore { .map(HeaderId::from)) } - pub fn get_last_block_id(&self) -> Result { - Ok(self.dbio.get_meta_last_block_in_db()?) + pub fn get_last_block_id(&self) -> Result> { + self.dbio.get_meta_last_block_id_in_db().map_err(Into::into) } pub fn get_block_at_id(&self, id: u64) -> Result> { @@ -86,18 +83,14 @@ impl IndexerStore { Ok(self.dbio.get_acc_transactions(acc_id, offset, limit)?) } - #[must_use] - pub fn genesis_id(&self) -> u64 { + pub fn genesis_id(&self) -> Result> { self.dbio - .get_meta_first_block_in_db() - .expect("Must be set at the DB startup") + .get_meta_first_block_id_in_db() + .map_err(Into::into) } - #[must_use] - pub fn last_block(&self) -> u64 { - self.dbio - .get_meta_last_block_in_db() - .expect("Must be set at the DB startup") + pub fn last_block(&self) -> Result> { + self.dbio.get_meta_last_block_id_in_db().map_err(Into::into) } pub fn get_state_at_block(&self, block_id: u64) -> Result { @@ -142,6 +135,7 @@ impl IndexerStore { } pub async fn put_block(&self, mut block: Block, l1_header: HeaderId) -> Result<()> { + info!("Applying block {}", block.header.block_id); { let mut state_guard = self.current_state.write().await; @@ -156,15 +150,32 @@ impl IndexerStore { "Last transaction in block must be the clock invocation for the block timestamp" ); + let is_genesis = block.header.block_id == 1; for transaction in user_txs { - transaction - .clone() - .transaction_stateless_check()? - .execute_check_on_state( - &mut state_guard, - block.header.block_id, - block.header.timestamp, - )?; + if is_genesis { + let genesis_tx = match transaction { + NSSATransaction::Public(public_tx) => public_tx, + NSSATransaction::PrivacyPreserving(_) + | NSSATransaction::ProgramDeployment(_) => { + anyhow::bail!("Genesis block should contain only public transactions") + } + }; + let state_diff = ValidatedStateDiff::from_public_genesis_transaction( + genesis_tx, + &state_guard, + ) + .context("Failed to create state diff from genesis transaction")?; + state_guard.apply_state_diff(state_diff); + } else { + transaction + .clone() + .transaction_stateless_check()? + .execute_check_on_state( + &mut state_guard, + block.header.block_id, + block.header.timestamp, + )?; + } } // Apply the clock invocation directly (it is expected to modify clock accounts). @@ -183,21 +194,19 @@ impl IndexerStore { // to represent correct block finality block.bedrock_status = BedrockStatus::Finalized; + info!("Putting block {} into DB", block.header.block_id); Ok(self.dbio.put_block(&block, l1_header.into())?) } } #[cfg(test)] mod tests { - use nssa::{AccountId, PublicKey}; + use common::{HashType, block::HashableBlockData}; + use nssa::{AccountId, CLOCK_01_PROGRAM_ACCOUNT_ID, PublicKey, PublicTransaction}; use tempfile::tempdir; use super::*; - fn genesis_block() -> Block { - common::test_utils::produce_dummy_block(1, None, vec![]) - } - fn acc1_sign_key() -> nssa::PrivateKey { nssa::PrivateKey::try_new([1; 32]).unwrap() } @@ -214,49 +223,59 @@ mod tests { AccountId::from(&PublicKey::new_from_private_key(&acc2_sign_key())) } + fn genesis_mint_tx(account: AccountId, balance: u128) -> NSSATransaction { + let message = nssa::public_transaction::Message::try_new( + nssa::program::Program::authenticated_transfer_program().id(), + vec![account, CLOCK_01_PROGRAM_ACCOUNT_ID], + vec![], + authenticated_transfer_core::Instruction::Mint { amount: balance }, + ) + .unwrap(); + let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]); + PublicTransaction::new(message, witness_set).into() + } + #[test] fn correct_startup() { let home = tempdir().unwrap(); - let storage = IndexerStore::open_db_with_genesis( - home.as_ref(), - &genesis_block(), - &nssa::V03State::new_with_genesis_accounts( - &[(acc1(), 10000), (acc2(), 20000)], - vec![], - 0, - ), - ) - .unwrap(); + let storage = IndexerStore::open_db(home.as_ref()).unwrap(); - let block = storage.get_block_at_id(1).unwrap().unwrap(); let final_id = storage.get_last_block_id().unwrap(); - assert_eq!(block.header.hash, genesis_block().header.hash); - assert_eq!(final_id, 1); + assert_eq!(final_id, None); } #[tokio::test] async fn state_transition() { let home = tempdir().unwrap(); - let storage = IndexerStore::open_db_with_genesis( - home.as_ref(), - &genesis_block(), - &nssa::V03State::new_with_genesis_accounts( - &[(acc1(), 10000), (acc2(), 20000)], - vec![], - 0, - ), - ) - .unwrap(); - - let mut prev_hash = genesis_block().header.hash; + let storage = IndexerStore::open_db(home.as_ref()).unwrap(); let from = acc1(); let to = acc2(); let sign_key = acc1_sign_key(); + // Submit genesis block + let clock_tx = NSSATransaction::Public(clock_invocation(0)); + let supply_from_tx = genesis_mint_tx(from, 10000); + let supply_to_tx = genesis_mint_tx(to, 20000); + let genesis_block_data = HashableBlockData { + block_id: 1, + prev_block_hash: HashType::default(), + timestamp: 0, + transactions: vec![supply_from_tx, supply_to_tx, clock_tx], + }; + let genesis_block = genesis_block_data.into_pending_block( + &common::test_utils::sequencer_sign_key_for_testing(), + [0; 32], + ); + let mut prev_hash = Some(genesis_block.header.hash); + storage + .put_block(genesis_block, HeaderId::from([0_u8; 32])) + .await + .unwrap(); + for i in 2..10 { let tx = common::test_utils::create_transaction_native_token_transfer( from, @@ -267,9 +286,8 @@ mod tests { ); let block_id = u64::try_from(i).unwrap(); - let next_block = - common::test_utils::produce_dummy_block(block_id, Some(prev_hash), vec![tx]); - prev_hash = next_block.header.hash; + let next_block = common::test_utils::produce_dummy_block(block_id, prev_hash, vec![tx]); + prev_hash = Some(next_block.header.hash); storage .put_block(next_block, HeaderId::from([u8::try_from(i).unwrap(); 32])) @@ -288,18 +306,9 @@ mod tests { async fn account_state_at_block() { let home = tempdir().unwrap(); - let storage = IndexerStore::open_db_with_genesis( - home.as_ref(), - &genesis_block(), - &nssa::V03State::new_with_genesis_accounts( - &[(acc1(), 10000), (acc2(), 20000)], - vec![], - 0, - ), - ) - .unwrap(); + let storage = IndexerStore::open_db(home.as_ref()).unwrap(); - let mut prev_hash = genesis_block().header.hash; + let mut prev_hash = None; let from = acc1(); let to = acc2(); @@ -315,9 +324,8 @@ mod tests { ); let block_id = u64::try_from(i).unwrap(); - let next_block = - common::test_utils::produce_dummy_block(block_id, Some(prev_hash), vec![tx]); - prev_hash = next_block.header.hash; + let next_block = common::test_utils::produce_dummy_block(block_id, prev_hash, vec![tx]); + prev_hash = Some(next_block.header.hash); storage .put_block(next_block, HeaderId::from([u8::try_from(i).unwrap(); 32])) diff --git a/indexer/core/src/config.rs b/indexer/core/src/config.rs index 40ac0870..6a019828 100644 --- a/indexer/core/src/config.rs +++ b/indexer/core/src/config.rs @@ -10,7 +10,6 @@ use common::config::BasicAuth; use humantime_serde; pub use logos_blockchain_core::mantle::ops::channel::ChannelId; use serde::{Deserialize, Serialize}; -use testnet_initial_state::{PrivateAccountPublicInitialData, PublicAccountPublicInitialData}; use url::Url; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -22,18 +21,12 @@ pub struct ClientConfig { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct IndexerConfig { - /// Home dir of sequencer storage. + /// Home dir of indexer storage. pub home: PathBuf, - /// Sequencers signing key. - pub signing_key: [u8; 32], #[serde(with = "humantime_serde")] pub consensus_info_polling_interval: Duration, pub bedrock_config: ClientConfig, pub channel_id: ChannelId, - #[serde(skip_serializing_if = "Option::is_none")] - pub initial_public_accounts: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub initial_private_accounts: Option>, } impl IndexerConfig { diff --git a/indexer/core/src/lib.rs b/indexer/core/src/lib.rs index 3d57e540..400d0a9d 100644 --- a/indexer/core/src/lib.rs +++ b/indexer/core/src/lib.rs @@ -1,17 +1,14 @@ use std::sync::Arc; use anyhow::Result; -use common::block::{Block, HashableBlockData}; +use common::block::Block; // ToDo: Remove after testnet -use common::{HashType, PINATA_BASE58}; use futures::StreamExt as _; use log::{error, info, warn}; use logos_blockchain_core::header::HeaderId; use logos_blockchain_zone_sdk::{ CommonHttpClient, ZoneMessage, adapter::NodeHttpClient, indexer::ZoneIndexer, }; -use nssa::V03State; -use testnet_initial_state::initial_state_testnet; use crate::{block_store::IndexerStore, config::IndexerConfig}; @@ -27,69 +24,6 @@ pub struct IndexerCore { impl IndexerCore { pub fn new(config: IndexerConfig) -> Result { - let hashable_data = HashableBlockData { - block_id: 1, - transactions: vec![], - prev_block_hash: HashType([0; 32]), - timestamp: 0, - }; - - // Genesis creation is fine as it is, - // because it will be overwritten by sequencer. - // Therefore: - // ToDo: remove key from indexer config, use some default. - let signing_key = nssa::PrivateKey::try_new(config.signing_key).unwrap(); - let channel_genesis_msg_id = [0; 32]; - let genesis_block = hashable_data.into_pending_block(&signing_key, channel_genesis_msg_id); - - let initial_private_accounts: Option> = - config.initial_private_accounts.as_ref().map(|accounts| { - accounts - .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(&account_id, &acc), - nssa_core::Nullifier::for_account_initialization(&account_id), - ) - }) - .collect() - }); - - let init_accs: Option> = config - .initial_public_accounts - .as_ref() - .map(|initial_accounts| { - initial_accounts - .iter() - .map(|acc_data| (acc_data.account_id, acc_data.balance)) - .collect() - }); - - // If initial commitments or accounts are present in config, need to construct state from - // them - let state = if initial_private_accounts.is_some() || init_accs.is_some() { - let mut state = V03State::new_with_genesis_accounts( - &init_accs.unwrap_or_default(), - initial_private_accounts.unwrap_or_default(), - genesis_block.header.timestamp, - ); - - // ToDo: Remove after testnet - state.add_pinata_program(PINATA_BASE58.parse().unwrap()); - - state - } else { - initial_state_testnet() - }; - let home = config.home.join("rocksdb"); let basic_auth = config.bedrock_config.auth.clone().map(Into::into); @@ -102,7 +36,7 @@ impl IndexerCore { Ok(Self { zone_indexer: Arc::new(zone_indexer), config, - store: IndexerStore::open_db_with_genesis(&home, &genesis_block, &state)?, + store: IndexerStore::open_db(&home)?, }) } diff --git a/indexer/ffi/indexer_ffi.h b/indexer/ffi/indexer_ffi.h index 7626b3b3..b2ba41bf 100644 --- a/indexer/ffi/indexer_ffi.h +++ b/indexer/ffi/indexer_ffi.h @@ -22,9 +22,10 @@ typedef enum FfiBedrockStatus { Finalized, } FfiBedrockStatus; +typedef struct Option_u64 Option_u64; + typedef struct IndexerServiceFFI { void *indexer_handle; - void *runtime; void *indexer_client; } IndexerServiceFFI; @@ -41,16 +42,56 @@ typedef struct PointerResult_IndexerServiceFFI__OperationStatus { typedef struct PointerResult_IndexerServiceFFI__OperationStatus InitializedIndexerServiceFFIResult; +typedef enum PointerKind_Tag { + Owned, + Borrowed, + Null, +} PointerKind_Tag; + +typedef struct PointerKind { + PointerKind_Tag tag; + union { + struct { + void *owned; + }; + struct { + const void *borrowed; + }; + }; +} PointerKind; + +typedef struct Pointer_Runtime { + struct PointerKind kind; +} Pointer_Runtime; + +/** + * Wrapper around [`tokio::runtime::Runtime`] that can be safely passed across the FFI boundary. + */ +typedef struct Runtime { + struct Pointer_Runtime inner; +} Runtime; + /** * 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_u64__OperationStatus { - uint64_t *value; +typedef struct PointerResult_Runtime__OperationStatus { + struct Runtime *value; enum OperationStatus error; -} PointerResult_u64__OperationStatus; +} PointerResult_Runtime__OperationStatus; + +/** + * 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_Option_u64_____OperationStatus { + struct Option_u64 *value; + enum OperationStatus error; +} PointerResult_Option_u64_____OperationStatus; typedef uint64_t FfiBlockId; @@ -379,8 +420,20 @@ typedef struct PointerResult_FfiVec_FfiTransaction_____OperationStatus { * * An `InitializedIndexerServiceFFIResult` containing either a pointer to the * initialized `IndexerServiceFFI` or an error code. + * + * # Safety + * The caller must ensure that: + * - `runtime` is a valid pointer to a `tokio::runtime::Runtime` instance. + * - `config_path` is a valid pointer to a null-terminated C string. */ -InitializedIndexerServiceFFIResult start_indexer(const char *config_path, uint16_t port); +InitializedIndexerServiceFFIResult start_indexer(const struct Runtime *runtime, + const char *config_path, + uint16_t port); + +/** + * Creates a new [`tokio::runtime::Runtime`]. + */ +struct PointerResult_Runtime__OperationStatus new_runtime(void); /** * Stops and frees the resources associated with the given indexer service. @@ -415,25 +468,27 @@ void free_cstring(char *block); * * # Arguments * - * - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. + * - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried. * * # Returns * - * A `PointerResult` indicating success or failure. + * A `PointerResult, OperationStatus>` indicating success or failure. * * # Safety * * The caller must ensure that: - * - `indexer` is a valid pointer to a `IndexerServiceFFI` instance + * - `runtime` is a valid pointer to a [`Runtime`] instance. + * - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. */ -struct PointerResult_u64__OperationStatus query_last_block(const struct IndexerServiceFFI *indexer); +struct PointerResult_Option_u64_____OperationStatus query_last_block(const struct Runtime *runtime, + const struct IndexerServiceFFI *indexer); /** * Query the block by id from indexer. * * # Arguments * - * - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. + * - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried. * - `block_id`: `u64` number of block id * * # Returns @@ -443,9 +498,11 @@ struct PointerResult_u64__OperationStatus query_last_block(const struct IndexerS * # Safety * * The caller must ensure that: - * - `indexer` is a valid pointer to a `IndexerServiceFFI` instance + * - `runtime` is a valid pointer to a [`Runtime`] instance. + * - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. */ -struct PointerResult_FfiBlockOpt__OperationStatus query_block(const struct IndexerServiceFFI *indexer, +struct PointerResult_FfiBlockOpt__OperationStatus query_block(const struct Runtime *runtime, + const struct IndexerServiceFFI *indexer, FfiBlockId block_id); /** @@ -453,7 +510,7 @@ struct PointerResult_FfiBlockOpt__OperationStatus query_block(const struct Index * * # Arguments * - * - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. + * - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried. * - `hash`: `FfiHashType` - hash of block * * # Returns @@ -463,9 +520,11 @@ struct PointerResult_FfiBlockOpt__OperationStatus query_block(const struct Index * # Safety * * The caller must ensure that: - * - `indexer` is a valid pointer to a `IndexerServiceFFI` instance + * - `runtime` is a valid pointer to a [`Runtime`] instance. + * - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. */ -struct PointerResult_FfiBlockOpt__OperationStatus query_block_by_hash(const struct IndexerServiceFFI *indexer, +struct PointerResult_FfiBlockOpt__OperationStatus query_block_by_hash(const struct Runtime *runtime, + const struct IndexerServiceFFI *indexer, FfiHashType hash); /** @@ -473,7 +532,7 @@ struct PointerResult_FfiBlockOpt__OperationStatus query_block_by_hash(const stru * * # Arguments * - * - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. + * - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried. * - `account_id`: `FfiAccountId` - id of queried account * * # Returns @@ -483,9 +542,11 @@ struct PointerResult_FfiBlockOpt__OperationStatus query_block_by_hash(const stru * # Safety * * The caller must ensure that: - * - `indexer` is a valid pointer to a `IndexerServiceFFI` instance + * - `runtime` is a valid pointer to a [`Runtime`] instance. + * - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. */ -struct PointerResult_FfiAccount__OperationStatus query_account(const struct IndexerServiceFFI *indexer, +struct PointerResult_FfiAccount__OperationStatus query_account(const struct Runtime *runtime, + const struct IndexerServiceFFI *indexer, FfiAccountId account_id); /** @@ -493,7 +554,7 @@ struct PointerResult_FfiAccount__OperationStatus query_account(const struct Inde * * # Arguments * - * - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. + * - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried. * - `hash`: `FfiHashType` - hash of transaction * * # Returns @@ -503,9 +564,11 @@ struct PointerResult_FfiAccount__OperationStatus query_account(const struct Inde * # Safety * * The caller must ensure that: - * - `indexer` is a valid pointer to a `IndexerServiceFFI` instance + * - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. + * - `runtime` is a valid pointer to a [`Runtime`] instance. */ -struct PointerResult_FfiOption_FfiTransaction_____OperationStatus query_transaction(const struct IndexerServiceFFI *indexer, +struct PointerResult_FfiOption_FfiTransaction_____OperationStatus query_transaction(const struct Runtime *runtime, + const struct IndexerServiceFFI *indexer, FfiHashType hash); /** @@ -513,7 +576,7 @@ struct PointerResult_FfiOption_FfiTransaction_____OperationStatus query_transact * * # Arguments * - * - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. + * - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried. * - `before`: `FfiOption` - end block of query * - `limit`: `u64` - number of blocks to query before `before` * @@ -524,9 +587,11 @@ struct PointerResult_FfiOption_FfiTransaction_____OperationStatus query_transact * # Safety * * The caller must ensure that: - * - `indexer` is a valid pointer to a `IndexerServiceFFI` instance + * - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. + * - `runtime` is a valid pointer to a [`Runtime`] instance. */ -struct PointerResult_FfiVec_FfiBlock_____OperationStatus query_block_vec(const struct IndexerServiceFFI *indexer, +struct PointerResult_FfiVec_FfiBlock_____OperationStatus query_block_vec(const struct Runtime *runtime, + const struct IndexerServiceFFI *indexer, struct FfiOption_u64 before, uint64_t limit); @@ -535,7 +600,7 @@ struct PointerResult_FfiVec_FfiBlock_____OperationStatus query_block_vec(const s * * # Arguments * - * - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. + * - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried. * - `account_id`: `FfiAccountId` - id of queried account * - `offset`: `u64` - first tx id of query * - `limit`: `u64` - number of tx ids to query after `offset` @@ -547,9 +612,11 @@ struct PointerResult_FfiVec_FfiBlock_____OperationStatus query_block_vec(const s * # Safety * * The caller must ensure that: - * - `indexer` is a valid pointer to a `IndexerServiceFFI` instance + * - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. + * - `runtime` is a valid pointer to a [`Runtime`] instance. */ -struct PointerResult_FfiVec_FfiTransaction_____OperationStatus query_transactions_by_account(const struct IndexerServiceFFI *indexer, +struct PointerResult_FfiVec_FfiTransaction_____OperationStatus query_transactions_by_account(const struct Runtime *runtime, + const struct IndexerServiceFFI *indexer, FfiAccountId account_id, uint64_t offset, uint64_t limit); diff --git a/indexer/ffi/src/api/lifecycle.rs b/indexer/ffi/src/api/lifecycle.rs index c9cd859d..d124901f 100644 --- a/indexer/ffi/src/api/lifecycle.rs +++ b/indexer/ffi/src/api/lifecycle.rs @@ -1,9 +1,7 @@ use std::{ffi::c_char, path::PathBuf}; -use tokio::runtime::Runtime; - use crate::{ - IndexerServiceFFI, + IndexerServiceFFI, Runtime, api::{ PointerResult, client::{UrlProtocol, addr_to_url}, @@ -26,17 +24,33 @@ pub type InitializedIndexerServiceFFIResult = PointerResult InitializedIndexerServiceFFIResult { - setup_indexer(config_path, port).map_or_else( + // SAFETY: The caller must ensure the validness of the `runtime` and `config_path` pointers. + unsafe { setup_indexer(runtime, config_path, port) }.map_or_else( InitializedIndexerServiceFFIResult::from_error, InitializedIndexerServiceFFIResult::from_value, ) } +/// Creates a new [`tokio::runtime::Runtime`]. +#[unsafe(no_mangle)] +pub extern "C" fn new_runtime() -> PointerResult { + Runtime::new().map_or_else( + |_e| PointerResult::from_error(OperationStatus::InitializationError), + PointerResult::from_value, + ) +} + /// Initializes and starts an indexer based on the provided /// configuration file path. /// @@ -49,7 +63,13 @@ pub extern "C" fn start_indexer( /// /// A `Result` containing either the initialized `IndexerServiceFFI` or an /// error code. -fn setup_indexer( +/// +/// # Safety +/// The caller must ensure that: +/// - `runtime` is a valid pointer to a `tokio::runtime::Runtime` instance. +/// - `config_path` is a valid pointer to a null-terminated C string. +unsafe fn setup_indexer( + runtime: *const Runtime, config_path: *const c_char, port: u16, ) -> Result { @@ -66,9 +86,11 @@ fn setup_indexer( OperationStatus::InitializationError })?; - let rt = Runtime::new().unwrap(); + // SAFETY: The caller must ensure that `runtime` is a valid pointer to a + // `tokio::runtime::Runtime` instance. + let runtime = unsafe { &*runtime }; - let indexer_handle = rt + let indexer_handle = runtime .block_on(indexer_service::run_server(config, port)) .map_err(|e| { log::error!("Could not start indexer service: {e}"); @@ -76,12 +98,14 @@ fn setup_indexer( })?; let indexer_url = addr_to_url(UrlProtocol::Ws, indexer_handle.addr())?; - let indexer_client = rt.block_on(IndexerClient::new(&indexer_url)).map_err(|e| { - log::error!("Could not start indexer client: {e}"); - OperationStatus::InitializationError - })?; + let indexer_client = runtime + .block_on(IndexerClient::new(&indexer_url)) + .map_err(|e| { + log::error!("Could not start indexer client: {e}"); + OperationStatus::InitializationError + })?; - Ok(IndexerServiceFFI::new(indexer_handle, rt, indexer_client)) + Ok(IndexerServiceFFI::new(indexer_handle, indexer_client)) } /// Stops and frees the resources associated with the given indexer service. diff --git a/indexer/ffi/src/api/query.rs b/indexer/ffi/src/api/query.rs index 1e39d961..44951014 100644 --- a/indexer/ffi/src/api/query.rs +++ b/indexer/ffi/src/api/query.rs @@ -2,7 +2,7 @@ use indexer_service_protocol::{AccountId, HashType}; use indexer_service_rpc::RpcClient as _; use crate::{ - IndexerServiceFFI, + IndexerServiceFFI, Runtime, api::{ PointerResult, types::{ @@ -19,20 +19,22 @@ use crate::{ /// /// # Arguments /// -/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. +/// - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried. /// /// # Returns /// -/// A `PointerResult` indicating success or failure. +/// A `PointerResult, OperationStatus>` indicating success or failure. /// /// # Safety /// /// The caller must ensure that: -/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance +/// - `runtime` is a valid pointer to a [`Runtime`] instance. +/// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. #[unsafe(no_mangle)] pub unsafe extern "C" fn query_last_block( + runtime: *const Runtime, indexer: *const IndexerServiceFFI, -) -> PointerResult { +) -> PointerResult, OperationStatus> { if indexer.is_null() { log::error!("Attempted to query a null indexer pointer. This is a bug. Aborting."); return PointerResult::from_error(OperationStatus::NullPointer); @@ -40,8 +42,8 @@ pub unsafe extern "C" fn query_last_block( let indexer = unsafe { &*indexer }; - let client = unsafe { indexer.client() }; - let runtime = unsafe { indexer.runtime() }; + let client = indexer.client(); + let runtime = unsafe { &*runtime }; runtime .block_on(client.get_last_finalized_block_id()) @@ -55,7 +57,7 @@ pub unsafe extern "C" fn query_last_block( /// /// # Arguments /// -/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. +/// - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried. /// - `block_id`: `u64` number of block id /// /// # Returns @@ -65,9 +67,11 @@ pub unsafe extern "C" fn query_last_block( /// # Safety /// /// The caller must ensure that: -/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance +/// - `runtime` is a valid pointer to a [`Runtime`] instance. +/// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. #[unsafe(no_mangle)] pub unsafe extern "C" fn query_block( + runtime: *const Runtime, indexer: *const IndexerServiceFFI, block_id: FfiBlockId, ) -> PointerResult { @@ -78,8 +82,8 @@ pub unsafe extern "C" fn query_block( let indexer = unsafe { &*indexer }; - let client = unsafe { indexer.client() }; - let runtime = unsafe { indexer.runtime() }; + let client = indexer.client(); + let runtime = unsafe { &*runtime }; runtime .block_on(client.get_block_by_id(block_id)) @@ -99,7 +103,7 @@ pub unsafe extern "C" fn query_block( /// /// # Arguments /// -/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. +/// - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried. /// - `hash`: `FfiHashType` - hash of block /// /// # Returns @@ -109,9 +113,11 @@ pub unsafe extern "C" fn query_block( /// # Safety /// /// The caller must ensure that: -/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance +/// - `runtime` is a valid pointer to a [`Runtime`] instance. +/// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. #[unsafe(no_mangle)] pub unsafe extern "C" fn query_block_by_hash( + runtime: *const Runtime, indexer: *const IndexerServiceFFI, hash: FfiHashType, ) -> PointerResult { @@ -122,8 +128,8 @@ pub unsafe extern "C" fn query_block_by_hash( let indexer = unsafe { &*indexer }; - let client = unsafe { indexer.client() }; - let runtime = unsafe { indexer.runtime() }; + let client = indexer.client(); + let runtime = unsafe { &*runtime }; runtime .block_on(client.get_block_by_hash(HashType(hash.data))) @@ -143,7 +149,7 @@ pub unsafe extern "C" fn query_block_by_hash( /// /// # Arguments /// -/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. +/// - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried. /// - `account_id`: `FfiAccountId` - id of queried account /// /// # Returns @@ -153,9 +159,11 @@ pub unsafe extern "C" fn query_block_by_hash( /// # Safety /// /// The caller must ensure that: -/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance +/// - `runtime` is a valid pointer to a [`Runtime`] instance. +/// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. #[unsafe(no_mangle)] pub unsafe extern "C" fn query_account( + runtime: *const Runtime, indexer: *const IndexerServiceFFI, account_id: FfiAccountId, ) -> PointerResult { @@ -166,8 +174,8 @@ pub unsafe extern "C" fn query_account( let indexer = unsafe { &*indexer }; - let client = unsafe { indexer.client() }; - let runtime = unsafe { indexer.runtime() }; + let client = indexer.client(); + let runtime = unsafe { &*runtime }; runtime .block_on(client.get_account(AccountId { @@ -187,7 +195,7 @@ pub unsafe extern "C" fn query_account( /// /// # Arguments /// -/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. +/// - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried. /// - `hash`: `FfiHashType` - hash of transaction /// /// # Returns @@ -197,9 +205,11 @@ pub unsafe extern "C" fn query_account( /// # Safety /// /// The caller must ensure that: -/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance +/// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. +/// - `runtime` is a valid pointer to a [`Runtime`] instance. #[unsafe(no_mangle)] pub unsafe extern "C" fn query_transaction( + runtime: *const Runtime, indexer: *const IndexerServiceFFI, hash: FfiHashType, ) -> PointerResult, OperationStatus> { @@ -210,8 +220,8 @@ pub unsafe extern "C" fn query_transaction( let indexer = unsafe { &*indexer }; - let client = unsafe { indexer.client() }; - let runtime = unsafe { indexer.runtime() }; + let client = indexer.client(); + let runtime = unsafe { &*runtime }; runtime .block_on(client.get_transaction(HashType(hash.data))) @@ -231,7 +241,7 @@ pub unsafe extern "C" fn query_transaction( /// /// # Arguments /// -/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. +/// - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried. /// - `before`: `FfiOption` - end block of query /// - `limit`: `u64` - number of blocks to query before `before` /// @@ -242,9 +252,11 @@ pub unsafe extern "C" fn query_transaction( /// # Safety /// /// The caller must ensure that: -/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance +/// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. +/// - `runtime` is a valid pointer to a [`Runtime`] instance. #[unsafe(no_mangle)] pub unsafe extern "C" fn query_block_vec( + runtime: *const Runtime, indexer: *const IndexerServiceFFI, before: FfiOption, limit: u64, @@ -256,8 +268,8 @@ pub unsafe extern "C" fn query_block_vec( let indexer = unsafe { &*indexer }; - let client = unsafe { indexer.client() }; - let runtime = unsafe { indexer.runtime() }; + let client = indexer.client(); + let runtime = unsafe { &*runtime }; let before_std = before.is_some.then(|| unsafe { *before.value }); @@ -281,7 +293,7 @@ pub unsafe extern "C" fn query_block_vec( /// /// # Arguments /// -/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. +/// - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried. /// - `account_id`: `FfiAccountId` - id of queried account /// - `offset`: `u64` - first tx id of query /// - `limit`: `u64` - number of tx ids to query after `offset` @@ -293,9 +305,11 @@ pub unsafe extern "C" fn query_block_vec( /// # Safety /// /// The caller must ensure that: -/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance +/// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. +/// - `runtime` is a valid pointer to a [`Runtime`] instance. #[unsafe(no_mangle)] pub unsafe extern "C" fn query_transactions_by_account( + runtime: *const Runtime, indexer: *const IndexerServiceFFI, account_id: FfiAccountId, offset: u64, @@ -308,8 +322,8 @@ pub unsafe extern "C" fn query_transactions_by_account( let indexer = unsafe { &*indexer }; - let client = unsafe { indexer.client() }; - let runtime = unsafe { indexer.runtime() }; + let client = indexer.client(); + let runtime = unsafe { &*runtime }; runtime .block_on(client.get_transactions_by_account( diff --git a/indexer/ffi/src/indexer.rs b/indexer/ffi/src/indexer.rs index 33800356..e8707697 100644 --- a/indexer/ffi/src/indexer.rs +++ b/indexer/ffi/src/indexer.rs @@ -1,53 +1,49 @@ use std::{ffi::c_void, net::SocketAddr}; use indexer_service::IndexerHandle; -use tokio::runtime::Runtime; use crate::client::IndexerClient; #[repr(C)] pub struct IndexerServiceFFI { indexer_handle: *mut c_void, - runtime: *mut c_void, indexer_client: *mut c_void, } impl IndexerServiceFFI { + #[must_use] pub fn new( indexer_handle: indexer_service::IndexerHandle, - runtime: Runtime, indexer_client: IndexerClient, ) -> 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::(), indexer_client: Box::into_raw(Box::new(indexer_client)).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, Box) { - let indexer_handle = unsafe { Box::from_raw(self.indexer_handle.cast::()) }; - let runtime = unsafe { Box::from_raw(self.runtime.cast::()) }; - let indexer_client = unsafe { Box::from_raw(self.indexer_client.cast::()) }; - (indexer_handle, runtime, indexer_client) + pub fn into_parts(mut self) -> (Box, Box) { + let Self { + indexer_handle, + indexer_client, + } = &mut self; + + let indexer_handle_boxed = unsafe { Box::from_raw(indexer_handle.cast::()) }; + let indexer_client_boxed = unsafe { Box::from_raw(indexer_client.cast::()) }; + + // Assigning nulls to prevent double free on drop, since ownership is transferred to caller + *indexer_handle = std::ptr::null_mut(); + *indexer_client = std::ptr::null_mut(); + + (indexer_handle_boxed, indexer_client_boxed) } /// 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 { + pub const fn addr(&self) -> SocketAddr { let indexer_handle = unsafe { self.indexer_handle .cast::() @@ -59,13 +55,8 @@ impl IndexerServiceFFI { } /// Helper to get indexer handle ref. - /// - /// # 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 { + pub const fn handle(&self) -> &IndexerHandle { unsafe { self.indexer_handle .cast::() @@ -75,13 +66,8 @@ impl IndexerServiceFFI { } /// Helper to get indexer client ref. - /// - /// # Safety - /// - /// The caller must ensure that: - /// - `self` is a valid object(contains valid pointers in all fields) #[must_use] - pub const unsafe fn client(&self) -> &IndexerClient { + pub const fn client(&self) -> &IndexerClient { unsafe { self.indexer_client .cast::() @@ -89,22 +75,6 @@ impl IndexerServiceFFI { .expect("Indexer Client must be non-null pointer") } } - - /// Helper to get indexer runtime ref. - /// - /// # Safety - /// - /// The caller must ensure that: - /// - `self` is a valid object(contains valid pointers in all fields) - #[must_use] - pub const unsafe fn runtime(&self) -> &Runtime { - unsafe { - self.runtime - .cast::() - .as_ref() - .expect("Indexer Runtime must be non-null pointer") - } - } } // Implement Drop to prevent memory leaks @@ -112,21 +82,14 @@ impl Drop for IndexerServiceFFI { fn drop(&mut self) { let Self { indexer_handle, - runtime, indexer_client, } = self; - if indexer_handle.is_null() { - log::error!("Attempted to drop a null indexer pointer. This is a bug"); + if !indexer_handle.is_null() { + drop(unsafe { Box::from_raw(indexer_handle.cast::()) }); } - if runtime.is_null() { - log::error!("Attempted to drop a null tokio runtime pointer. This is a bug"); + if !indexer_client.is_null() { + drop(unsafe { Box::from_raw(indexer_client.cast::()) }); } - if indexer_client.is_null() { - log::error!("Attempted to drop a null client pointer. This is a bug"); - } - drop(unsafe { Box::from_raw(indexer_handle.cast::()) }); - drop(unsafe { Box::from_raw(runtime.cast::()) }); - drop(unsafe { Box::from_raw(indexer_client.cast::()) }); } } diff --git a/indexer/ffi/src/lib.rs b/indexer/ffi/src/lib.rs index 5806a074..9e34b111 100644 --- a/indexer/ffi/src/lib.rs +++ b/indexer/ffi/src/lib.rs @@ -2,8 +2,10 @@ pub use errors::OperationStatus; pub use indexer::IndexerServiceFFI; +pub use runtime::Runtime; pub mod api; mod client; mod errors; mod indexer; +mod runtime; diff --git a/indexer/ffi/src/runtime.rs b/indexer/ffi/src/runtime.rs new file mode 100644 index 00000000..ba361fd8 --- /dev/null +++ b/indexer/ffi/src/runtime.rs @@ -0,0 +1,129 @@ +use std::ffi::c_void; + +/// Wrapper around [`tokio::runtime::Runtime`] that can be safely passed across the FFI boundary. +#[repr(C)] +pub struct Runtime { + inner: Pointer, +} + +impl Runtime { + /// Creates a new owned [`Runtime`] instance. + pub fn new() -> Result> { + let inner = tokio::runtime::Runtime::new()?; + Ok(Self { + inner: Pointer::owned(inner), + }) + } + + /// Creates a new owned [`Runtime`] instance from an existing [`tokio::runtime::Runtime`]. + pub fn from_owned(inner: tokio::runtime::Runtime) -> Self { + Self { + inner: Pointer::owned(inner), + } + } + + /// Creates a new borrowed [`Runtime`] instance from a reference to an existing + /// `tokio::runtime::Runtime`. + /// + /// # Safety + /// The caller must ensure that the provided reference remains valid for the lifetime of the + /// returned [`Runtime`]. + pub const unsafe fn from_borrowed(inner: &tokio::runtime::Runtime) -> Self { + Self { + // SAFETY: The caller must ensure the validness of the `inner` reference. + inner: unsafe { Pointer::borrowed(inner) }, + } + } +} + +impl AsRef for Runtime { + fn as_ref(&self) -> &tokio::runtime::Runtime { + self.inner + .as_ref() + .expect("Runtime pointer should not be null") + } +} + +impl std::ops::Deref for Runtime { + type Target = tokio::runtime::Runtime; + + fn deref(&self) -> &Self::Target { + self.as_ref() + } +} + +#[repr(C)] +struct Pointer { + kind: PointerKind, + _marker: std::marker::PhantomData, +} + +#[repr(C)] +enum PointerKind { + Owned(*mut c_void), + Borrowed(*const c_void), + Null, +} + +impl Pointer { + /// Creates a new owned pointer from a value. + pub fn owned(value: T) -> Self { + let boxed = Box::new(value); + let kind = PointerKind::Owned(Box::into_raw(boxed).cast::()); + Self { + kind, + _marker: std::marker::PhantomData, + } + } + + /// Creates a new borrowed pointer from a reference to an existing value. + /// + /// # Safety + /// The caller must ensure that the provided reference remains valid for the lifetime of the + /// returned pointer. + pub const unsafe fn borrowed(value: &T) -> Self { + let kind = PointerKind::Borrowed(std::ptr::from_ref(value).cast::()); + Self { + kind, + _marker: std::marker::PhantomData, + } + } + + /// Returns a reference to the value if the pointer is owned or borrowed, or [`None`] if it is + /// null. + pub const fn as_ref(&self) -> Option<&T> { + match self.kind { + PointerKind::Owned(ptr) => unsafe { (ptr.cast::()).as_ref() }, + PointerKind::Borrowed(ptr) => unsafe { (ptr.cast::()).as_ref() }, + PointerKind::Null => None, + } + } + + /// Takes ownership of the pointer if it is owned, returning the raw pointer and leaving a null + /// pointer in its place. + /// If the pointer is borrowed or null, returns [`None`]. + #[expect(dead_code, reason = "May be useful in future")] + pub fn take(&mut self) -> Option { + match std::mem::replace(&mut self.kind, PointerKind::Null) { + PointerKind::Owned(ptr) => { + // SAFETY: We ensure that the pointer is valid and was allocated by us. + let boxed = unsafe { Box::from_raw(ptr.cast::()) }; + Some(*boxed) + } + PointerKind::Borrowed(_) | PointerKind::Null => None, + } + } +} + +impl Drop for Pointer { + fn drop(&mut self) { + let Self { kind, _marker } = self; + + if let PointerKind::Owned(ptr) = *kind { + // SAFETY: We ensure that the pointer is valid and was allocated by us. + unsafe { + drop(Box::from_raw(ptr.cast::())); + } + } + } +} diff --git a/indexer/service/configs/indexer_config.json b/indexer/service/configs/indexer_config.json index 558a3bfe..f6a0e07c 100644 --- a/indexer/service/configs/indexer_config.json +++ b/indexer/service/configs/indexer_config.json @@ -4,153 +4,5 @@ "bedrock_config": { "addr": "http://localhost:8080" }, - "channel_id": "0101010101010101010101010101010101010101010101010101010101010101", - "initial_accounts": [ - { - "account_id": "CbgR6tj5kWx5oziiFptM7jMvrQeYY3Mzaao6ciuhSr2r", - "balance": 10000 - }, - { - "account_id": "2RHZhw9h534Zr3eq2RGhQete2Hh667foECzXPmSkGni2", - "balance": 20000 - } - ], - "initial_commitments": [ - { - "npk": [ - 139, - 19, - 158, - 11, - 155, - 231, - 85, - 206, - 132, - 228, - 220, - 114, - 145, - 89, - 113, - 156, - 238, - 142, - 242, - 74, - 182, - 91, - 43, - 100, - 6, - 190, - 31, - 15, - 31, - 88, - 96, - 204 - ], - "account": { - "program_owner": [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - "balance": 10000, - "data": [], - "nonce": 0 - } - }, - { - "npk": [ - 173, - 134, - 33, - 223, - 54, - 226, - 10, - 71, - 215, - 254, - 143, - 172, - 24, - 244, - 243, - 208, - 65, - 112, - 118, - 70, - 217, - 240, - 69, - 100, - 129, - 3, - 121, - 25, - 213, - 132, - 42, - 45 - ], - "account": { - "program_owner": [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - "balance": 20000, - "data": [], - "nonce": 0 - } - } - ], - "signing_key": [ - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37, - 37 - ] + "channel_id": "0101010101010101010101010101010101010101010101010101010101010101" } \ No newline at end of file diff --git a/indexer/service/rpc/src/lib.rs b/indexer/service/rpc/src/lib.rs index a6476ebd..5763fe82 100644 --- a/indexer/service/rpc/src/lib.rs +++ b/indexer/service/rpc/src/lib.rs @@ -27,7 +27,7 @@ pub trait Rpc { async fn subscribe_to_finalized_blocks(&self) -> SubscriptionResult; #[method(name = "getLastFinalizedBlockId")] - async fn get_last_finalized_block_id(&self) -> Result; + async fn get_last_finalized_block_id(&self) -> Result, ErrorObjectOwned>; #[method(name = "getBlockById")] async fn get_block_by_id(&self, block_id: BlockId) -> Result, ErrorObjectOwned>; diff --git a/indexer/service/src/lib.rs b/indexer/service/src/lib.rs index 10f1cade..b0a6e516 100644 --- a/indexer/service/src/lib.rs +++ b/indexer/service/src/lib.rs @@ -16,6 +16,7 @@ pub struct IndexerHandle { /// Option because of `Drop` which forbids to simply move out of `self` in `stopped()`. server_handle: Option, } + impl IndexerHandle { const fn new(addr: SocketAddr, server_handle: ServerHandle) -> Self { Self { diff --git a/indexer/service/src/mock_service.rs b/indexer/service/src/mock_service.rs index a413973d..a83e9ccc 100644 --- a/indexer/service/src/mock_service.rs +++ b/indexer/service/src/mock_service.rs @@ -190,18 +190,16 @@ impl indexer_service_rpc::RpcServer for MockIndexerService { Ok(()) } - async fn get_last_finalized_block_id(&self) -> Result { - self.state + async fn get_last_finalized_block_id(&self) -> Result, ErrorObjectOwned> { + Ok(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::<()>) - }) + .map(|block| block.header.block_id)) } async fn get_block_by_id(&self, block_id: BlockId) -> Result, ErrorObjectOwned> { diff --git a/indexer/service/src/service.rs b/indexer/service/src/service.rs index 8d079265..a959b80c 100644 --- a/indexer/service/src/service.rs +++ b/indexer/service/src/service.rs @@ -48,7 +48,7 @@ impl indexer_service_rpc::RpcServer for IndexerService { Ok(()) } - async fn get_last_finalized_block_id(&self) -> Result { + async fn get_last_finalized_block_id(&self) -> Result, ErrorObjectOwned> { self.indexer.store.get_last_block_id().map_err(db_error) } @@ -214,43 +214,49 @@ impl SubscriptionService { tokio::sync::mpsc::unbounded_channel::>(); let handle = tokio::spawn(async move { - let mut subscribers = Vec::new(); + let run_loop = async { + let mut subscribers = Vec::new(); - let mut block_stream = pin!(indexer.subscribe_parse_block_stream()); + let mut block_stream = pin!(indexer.subscribe_parse_block_stream()); - #[expect( - clippy::integer_division_remainder_used, - reason = "Generated by select! macro, can't be easily rewritten to avoid this lint" - )] - loop { - tokio::select! { - sub = sub_receiver.recv() => { - let Some(subscription) = sub else { - bail!("Subscription receiver closed unexpectedly"); - }; - info!("Added new subscription with ID {:?}", subscription.sink.subscription_id()); - subscribers.push(subscription); - } - block_opt = block_stream.next() => { - debug!("Got new block from block stream"); - let Some(block) = block_opt else { - bail!("Block stream ended unexpectedly"); - }; - let block = block.context("Failed to get L2 block data")?; - let block: indexer_service_protocol::Block = block.into(); + #[expect( + clippy::integer_division_remainder_used, + reason = "Generated by select! macro, can't be easily rewritten to avoid this lint" + )] + loop { + tokio::select! { + sub = sub_receiver.recv() => { + let Some(subscription) = sub else { + bail!("Subscription receiver closed unexpectedly"); + }; + info!("Added new subscription with ID {:?}", subscription.sink.subscription_id()); + subscribers.push(subscription); + } + block_opt = block_stream.next() => { + debug!("Got new block from block stream"); + let Some(block) = block_opt else { + bail!("Block stream ended unexpectedly"); + }; + let block = block.context("Failed to get L2 block data")?; + let block: indexer_service_protocol::Block = block.into(); - for sub in &mut subscribers { - if let Err(err) = sub.try_send(&block.header.block_id) { - warn!( - "Failed to send block ID {:?} to subscription ID {:?} with error: {err:#?}", - block.header.block_id, - sub.sink.subscription_id(), - ); + for sub in &mut subscribers { + if let Err(err) = sub.try_send(&block.header.block_id) { + warn!( + "Failed to send block ID {:?} to subscription ID {:?} with error: {err:#?}", + block.header.block_id, + sub.sink.subscription_id(), + ); + } } } } } - } + }; + let res: anyhow::Result = run_loop.await; + let Err(err) = res; + error!("Subscription service loop has unexpectedly finished with error: {err:#?}"); + Err(err) }); SubscriptionLoopParts { handle, diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index 5f1f1037..50596f37 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -10,6 +10,7 @@ workspace = true [dependencies] nssa_core = { workspace = true, features = ["host"] } nssa.workspace = true +authenticated_transfer_core.workspace = true sequencer_core = { workspace = true, features = ["default", "testnet"] } sequencer_service.workspace = true wallet.workspace = true @@ -28,7 +29,6 @@ testnet_initial_state.workspace = true indexer_service_protocol.workspace = true url.workspace = true - anyhow.workspace = true env_logger.workspace = true log.workspace = true diff --git a/integration_tests/src/config.rs b/integration_tests/src/config.rs index 7b3825de..9c0db5df 100644 --- a/integration_tests/src/config.rs +++ b/integration_tests/src/config.rs @@ -3,16 +3,13 @@ use std::{net::SocketAddr, path::PathBuf, time::Duration}; use anyhow::{Context as _, Result}; use bytesize::ByteSize; use indexer_service::{ChannelId, ClientConfig, IndexerConfig}; -use key_protocol::key_management::KeyChain; -use nssa::{Account, AccountId, PrivateKey, PublicKey}; -use nssa_core::{account::Data, program::DEFAULT_PROGRAM_ID}; -use sequencer_core::config::{BedrockConfig, SequencerConfig}; -use testnet_initial_state::{ - PrivateAccountPrivateInitialData, PrivateAccountPublicInitialData, - PublicAccountPrivateInitialData, PublicAccountPublicInitialData, -}; +use nssa::{AccountId, PrivateKey, PublicKey}; +use sequencer_core::config::{BedrockConfig, GenesisTransaction, SequencerConfig}; use url::Url; -use wallet::config::{InitialAccountData, WalletConfig}; +use wallet::config::WalletConfig; + +pub const INITIAL_PUBLIC_BALANCES_FOR_WALLET: [u128; 2] = [20_000, 40_000]; +pub const INITIAL_PRIVATE_BALANCES_FOR_WALLET: [u128; 2] = [10_000, 20_000]; /// Sequencer config options available for custom changes in integration tests. #[derive(Debug, Clone, Copy)] @@ -34,121 +31,6 @@ impl Default for SequencerPartialConfig { } } -pub struct InitialData { - pub public_accounts: Vec<(PrivateKey, u128)>, - pub private_accounts: Vec<(KeyChain, Account)>, -} - -impl InitialData { - #[must_use] - pub fn with_two_public_and_two_private_initialized_accounts() -> Self { - let mut public_alice_private_key = PrivateKey::new_os_random(); - let mut public_alice_public_key = - PublicKey::new_from_private_key(&public_alice_private_key); - let mut public_alice_account_id = AccountId::from(&public_alice_public_key); - - let mut public_bob_private_key = PrivateKey::new_os_random(); - let mut public_bob_public_key = PublicKey::new_from_private_key(&public_bob_private_key); - let mut public_bob_account_id = AccountId::from(&public_bob_public_key); - - // Ensure consistent ordering - if public_alice_account_id > public_bob_account_id { - std::mem::swap(&mut public_alice_private_key, &mut public_bob_private_key); - std::mem::swap(&mut public_alice_public_key, &mut public_bob_public_key); - std::mem::swap(&mut public_alice_account_id, &mut public_bob_account_id); - } - - 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, 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, 0)); - - // Ensure consistent ordering - if private_charlie_account_id > private_david_account_id { - std::mem::swap(&mut private_charlie_key_chain, &mut private_david_key_chain); - std::mem::swap( - &mut private_charlie_account_id, - &mut private_david_account_id, - ); - } - - Self { - public_accounts: vec![ - (public_alice_private_key, 10_000), - (public_bob_private_key, 20_000), - ], - private_accounts: vec![ - ( - private_charlie_key_chain, - Account { - balance: 10_000, - data: Data::default(), - program_owner: DEFAULT_PROGRAM_ID, - nonce: 0_u128.into(), - }, - ), - ( - private_david_key_chain, - Account { - balance: 20_000, - data: Data::default(), - program_owner: DEFAULT_PROGRAM_ID, - nonce: 0_u128.into(), - }, - ), - ], - } - } - - fn sequencer_initial_public_accounts(&self) -> Vec { - self.public_accounts - .iter() - .map(|(priv_key, balance)| { - let pub_key = PublicKey::new_from_private_key(priv_key); - let account_id = AccountId::from(&pub_key); - PublicAccountPublicInitialData { - account_id, - balance: *balance, - } - }) - .collect() - } - - fn sequencer_initial_private_accounts(&self) -> Vec { - self.private_accounts - .iter() - .map(|(key_chain, account)| PrivateAccountPublicInitialData { - npk: key_chain.nullifier_public_key, - account: account.clone(), - }) - .collect() - } - - fn wallet_initial_accounts(&self) -> Vec { - self.public_accounts - .iter() - .map(|(priv_key, _)| { - let pub_key = PublicKey::new_from_private_key(priv_key); - let account_id = AccountId::from(&pub_key); - InitialAccountData::Public(PublicAccountPrivateInitialData { - account_id, - pub_sign_key: priv_key.clone(), - }) - }) - .chain(self.private_accounts.iter().map(|(key_chain, account)| { - InitialAccountData::Private(Box::new(PrivateAccountPrivateInitialData { - account: account.clone(), - key_chain: key_chain.clone(), - identifier: 0, - })) - })) - .collect() - } -} - #[derive(Debug, Clone, Copy)] pub enum UrlProtocol { Http, @@ -168,7 +50,7 @@ pub fn sequencer_config( partial: SequencerPartialConfig, home: PathBuf, bedrock_addr: SocketAddr, - initial_data: &InitialData, + genesis_transactions: Vec, ) -> Result { let SequencerPartialConfig { max_num_tx_in_block, @@ -179,15 +61,13 @@ pub fn sequencer_config( Ok(SequencerConfig { home, - genesis_id: 1, is_genesis_random: true, max_num_tx_in_block, max_block_size, mempool_max_size, block_create_timeout, retry_pending_blocks_timeout: Duration::from_secs(5), - initial_public_accounts: Some(initial_data.sequencer_initial_public_accounts()), - initial_private_accounts: Some(initial_data.sequencer_initial_private_accounts()), + genesis: genesis_transactions, signing_key: [37; 32], bedrock_config: BedrockConfig { channel_id: bedrock_channel_id(), @@ -198,10 +78,46 @@ pub fn sequencer_config( }) } -pub fn wallet_config( - sequencer_addr: SocketAddr, - initial_data: &InitialData, -) -> Result { +#[must_use] +pub fn default_public_accounts_for_wallet() -> Vec<(PrivateKey, u128)> { + let mut first_private_key = PrivateKey::new_os_random(); + let first_public_key = PublicKey::new_from_private_key(&first_private_key); + let mut first_account_id = AccountId::from(&first_public_key); + + let mut second_private_key = PrivateKey::new_os_random(); + let second_public_key = PublicKey::new_from_private_key(&second_private_key); + let mut second_account_id = AccountId::from(&second_public_key); + + // Keep account ordering deterministic for tests that index into account lists. + if first_account_id > second_account_id { + std::mem::swap(&mut first_private_key, &mut second_private_key); + std::mem::swap(&mut first_account_id, &mut second_account_id); + } + + vec![ + (first_private_key, INITIAL_PUBLIC_BALANCES_FOR_WALLET[0]), + (second_private_key, INITIAL_PUBLIC_BALANCES_FOR_WALLET[1]), + ] +} + +#[must_use] +pub fn genesis_from_public_accounts( + public_accounts: &[(PrivateKey, u128)], +) -> Vec { + public_accounts + .iter() + .map(|(private_key, balance)| { + let public_key = PublicKey::new_from_private_key(private_key); + let account_id = AccountId::from(&public_key); + GenesisTransaction::SupplyPublicAccount { + account_id, + balance: *balance, + } + }) + .collect() +} + +pub fn wallet_config(sequencer_addr: SocketAddr) -> Result { Ok(WalletConfig { sequencer_addr: addr_to_url(UrlProtocol::Http, sequencer_addr) .context("Failed to convert sequencer addr to URL")?, @@ -209,16 +125,11 @@ pub fn wallet_config( seq_tx_poll_max_blocks: 15, seq_poll_max_retries: 10, seq_block_poll_max_amount: 100, - initial_accounts: Some(initial_data.wallet_initial_accounts()), basic_auth: None, }) } -pub fn indexer_config( - bedrock_addr: SocketAddr, - home: PathBuf, - initial_data: &InitialData, -) -> Result { +pub fn indexer_config(bedrock_addr: SocketAddr, home: PathBuf) -> Result { Ok(IndexerConfig { home, consensus_info_polling_interval: Duration::from_secs(1), @@ -227,9 +138,6 @@ pub fn indexer_config( .context("Failed to convert bedrock addr to URL")?, auth: None, }, - initial_public_accounts: Some(initial_data.sequencer_initial_public_accounts()), - initial_private_accounts: Some(initial_data.sequencer_initial_private_accounts()), - signing_key: [37; 32], channel_id: bedrock_channel_id(), }) } diff --git a/integration_tests/src/lib.rs b/integration_tests/src/lib.rs index 2a9e7c67..f69cdafd 100644 --- a/integration_tests/src/lib.rs +++ b/integration_tests/src/lib.rs @@ -1,6 +1,6 @@ //! This library contains common code for integration tests. -use std::sync::LazyLock; +use std::{net::SocketAddr, sync::LazyLock}; use anyhow::{Context as _, Result}; use common::{HashType, transaction::NSSATransaction}; @@ -9,21 +9,24 @@ use indexer_service::IndexerHandle; use log::{debug, error}; use nssa::{AccountId, PrivacyPreservingTransaction}; use nssa_core::Commitment; +use sequencer_core::config::GenesisTransaction; use sequencer_service::SequencerHandle; use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder}; use tempfile::TempDir; use testcontainers::compose::DockerCompose; -use wallet::WalletCore; +use wallet::{WalletCore, account::AccountIdWithPrivacy, cli::CliAccountMention}; use crate::{ indexer_client::IndexerClient, - setup::{setup_bedrock_node, setup_indexer, setup_sequencer, setup_wallet}, + setup::{ + setup_bedrock_node, setup_indexer, setup_private_accounts_with_initial_supply, + setup_sequencer, setup_wallet, + }, }; pub mod config; pub mod indexer_client; 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; @@ -35,6 +38,26 @@ const BEDROCK_SERVICE_PORT: u16 = 18080; static LOGGER: LazyLock<()> = LazyLock::new(env_logger::init); +struct IndexerComponents { + indexer_handle: IndexerHandle, + indexer_client: IndexerClient, + _temp_dir: TempDir, +} + +impl Drop for IndexerComponents { + fn drop(&mut self) { + let Self { + indexer_handle, + indexer_client: _, + _temp_dir: _, + } = self; + + if !indexer_handle.is_healthy() { + error!("Indexer handle has unexpectedly stopped before IndexerComponents drop"); + } + } +} + /// Test context which sets up a sequencer and a wallet for integration tests. /// /// It's memory and logically safe to create multiple instances of this struct in parallel tests, @@ -42,14 +65,13 @@ static LOGGER: LazyLock<()> = LazyLock::new(env_logger::init); // NOTE: Order of fields is important for proper drop order. pub struct TestContext { sequencer_client: SequencerClient, - indexer_client: IndexerClient, wallet: WalletCore, wallet_password: String, /// Optional to move out value in Drop. sequencer_handle: Option, - indexer_handle: IndexerHandle, + indexer_components: Option, bedrock_compose: DockerCompose, - _temp_indexer_dir: TempDir, + bedrock_addr: SocketAddr, _temp_sequencer_dir: TempDir, _temp_wallet_dir: TempDir, } @@ -60,61 +82,12 @@ impl TestContext { Self::builder().build().await } + /// Get a builder for the test context to customize its configuration. #[must_use] pub const fn builder() -> TestContextBuilder { TestContextBuilder::new() } - async fn new_configured( - sequencer_partial_config: config::SequencerPartialConfig, - initial_data: config::InitialData, - ) -> Result { - // Ensure logger is initialized only once - *LOGGER; - - debug!("Test context setup"); - - let (bedrock_compose, bedrock_addr) = setup_bedrock_node().await?; - - let (indexer_handle, temp_indexer_dir) = setup_indexer(bedrock_addr, &initial_data) - .await - .context("Failed to setup Indexer")?; - - let (sequencer_handle, temp_sequencer_dir) = - setup_sequencer(sequencer_partial_config, bedrock_addr, &initial_data) - .await - .context("Failed to setup Sequencer")?; - - let (wallet, temp_wallet_dir, wallet_password) = - setup_wallet(sequencer_handle.addr(), &initial_data) - .await - .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, indexer_handle.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 = IndexerClient::new(&indexer_url) - .await - .context("Failed to create indexer client")?; - - Ok(Self { - sequencer_client, - indexer_client, - wallet, - wallet_password, - bedrock_compose, - sequencer_handle: Some(sequencer_handle), - indexer_handle, - _temp_indexer_dir: temp_indexer_dir, - _temp_sequencer_dir: temp_sequencer_dir, - _temp_wallet_dir: temp_wallet_dir, - }) - } - /// Get reference to the wallet. #[must_use] pub const fn wallet(&self) -> &WalletCore { @@ -137,10 +110,38 @@ impl TestContext { &self.sequencer_client } - /// Get reference to the indexer client. + /// Get the Bedrock Node address. #[must_use] - pub const fn indexer_client(&self) -> &IndexerClient { - &self.indexer_client + pub const fn bedrock_addr(&self) -> SocketAddr { + self.bedrock_addr + } + + /// Get reference to the indexer. + /// + /// # Panics + /// + /// Panics if the indexer is not enabled in the test context. See + /// [`TestContextBuilder::disable_indexer()`]. + #[must_use] + pub fn indexer(&self) -> &IndexerHandle { + self.indexer_components + .as_ref() + .map(|components| &components.indexer_handle) + .expect("Called `TestContext::indexer()` on context with disabled indexer") + } + + /// Get reference to the indexer client. + /// + /// # Panics + /// + /// Panics if the indexer is not enabled in the test context. See + /// [`TestContextBuilder::disable_indexer()`]. + #[must_use] + pub fn indexer_client(&self) -> &IndexerClient { + self.indexer_components + .as_ref() + .map(|components| &components.indexer_client) + .expect("Called `TestContext::indexer_client()` on context with disabled indexer") } /// Get existing public account IDs in the wallet. @@ -148,8 +149,9 @@ impl TestContext { pub fn existing_public_accounts(&self) -> Vec { self.wallet .storage() - .user_data + .key_chain() .public_account_ids() + .map(|(account_id, _idx)| account_id) .collect() } @@ -158,8 +160,9 @@ impl TestContext { pub fn existing_private_accounts(&self) -> Vec { self.wallet .storage() - .user_data + .key_chain() .private_account_ids() + .map(|(account_id, _idx)| account_id) .collect() } } @@ -168,15 +171,14 @@ impl Drop for TestContext { fn drop(&mut self) { let Self { sequencer_handle, - indexer_handle, bedrock_compose, - _temp_indexer_dir: _, - _temp_sequencer_dir: _, - _temp_wallet_dir: _, + bedrock_addr: _, + indexer_components: _, sequencer_client: _, - indexer_client: _, wallet: _, wallet_password: _, + _temp_sequencer_dir: _, + _temp_wallet_dir: _, } = self; let sequencer_handle = sequencer_handle @@ -192,10 +194,6 @@ impl Drop for TestContext { ); } - if !indexer_handle.is_healthy() { - error!("Indexer handle has unexpectedly stopped before TestContext drop"); - } - let container = bedrock_compose .service(BEDROCK_SERVICE_WITH_OPEN_PORT) .unwrap_or_else(|| { @@ -216,43 +214,24 @@ impl Drop for TestContext { } } -/// A test context to be used in normal #[test] tests. -pub struct BlockingTestContext { - ctx: Option, - runtime: tokio::runtime::Runtime, -} - -impl BlockingTestContext { - pub fn new() -> Result { - let runtime = tokio::runtime::Runtime::new().unwrap(); - let ctx = runtime.block_on(TestContext::new())?; - Ok(Self { - ctx: Some(ctx), - runtime, - }) - } - - pub const fn ctx(&self) -> &TestContext { - self.ctx.as_ref().expect("TestContext is set") - } -} - pub struct TestContextBuilder { - initial_data: Option, + genesis_transactions: Option>, sequencer_partial_config: Option, + enable_indexer: bool, } impl TestContextBuilder { const fn new() -> Self { Self { - initial_data: None, + genesis_transactions: None, sequencer_partial_config: None, + enable_indexer: false, } } #[must_use] - pub fn with_initial_data(mut self, initial_data: config::InitialData) -> Self { - self.initial_data = Some(initial_data); + pub fn with_genesis(mut self, genesis_transactions: Vec) -> Self { + self.genesis_transactions = Some(genesis_transactions); self } @@ -265,14 +244,135 @@ impl TestContextBuilder { self } + /// Exclude Indexer from test context. + /// Indexer is enabled by default. + /// + /// Methods like [`TestContext::indexer()`] and [`TestContext::indexer_client()`] will panic if + /// called when indexer is disabled. + #[must_use] + pub const fn disable_indexer(mut self) -> Self { + self.enable_indexer = false; + self + } + pub async fn build(self) -> Result { - TestContext::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() - }), + let Self { + genesis_transactions, + sequencer_partial_config, + enable_indexer, + } = self; + + // Ensure logger is initialized only once + *LOGGER; + + debug!("Test context setup"); + + let (bedrock_compose, bedrock_addr) = setup_bedrock_node() + .await + .context("Failed to setup Bedrock node")?; + + let indexer_components = if enable_indexer { + let (indexer_handle, temp_indexer_dir) = setup_indexer(bedrock_addr) + .await + .context("Failed to setup Indexer")?; + let indexer_url = config::addr_to_url(config::UrlProtocol::Ws, indexer_handle.addr()) + .context("Failed to convert indexer addr to URL")?; + let indexer_client = IndexerClient::new(&indexer_url) + .await + .context("Failed to create indexer client")?; + Some(IndexerComponents { + indexer_handle, + indexer_client, + _temp_dir: temp_indexer_dir, + }) + } else { + None + }; + + let initial_public_accounts = config::default_public_accounts_for_wallet(); + let (sequencer_handle, temp_sequencer_dir) = setup_sequencer( + sequencer_partial_config.unwrap_or_default(), + bedrock_addr, + genesis_transactions + .unwrap_or_else(|| config::genesis_from_public_accounts(&initial_public_accounts)), ) .await + .context("Failed to setup Sequencer")?; + + let (mut wallet, temp_wallet_dir, wallet_password) = + setup_wallet(sequencer_handle.addr(), &initial_public_accounts) + .context("Failed to setup wallet")?; + setup_private_accounts_with_initial_supply(&mut wallet) + .await + .context("Failed to initialize private accounts in wallet")?; + + let sequencer_url = config::addr_to_url(config::UrlProtocol::Http, sequencer_handle.addr()) + .context("Failed to convert sequencer addr to URL")?; + let sequencer_client = SequencerClientBuilder::default() + .build(sequencer_url) + .context("Failed to create sequencer client")?; + + Ok(TestContext { + sequencer_client, + wallet, + wallet_password, + bedrock_compose, + bedrock_addr, + sequencer_handle: Some(sequencer_handle), + indexer_components, + _temp_sequencer_dir: temp_sequencer_dir, + _temp_wallet_dir: temp_wallet_dir, + }) + } + + pub fn build_blocking(self) -> Result { + let runtime = tokio::runtime::Runtime::new().context("Failed to create Tokio runtime")?; + + let ctx = runtime.block_on(self.build())?; + + Ok(BlockingTestContext { + ctx: Some(ctx), + runtime, + }) + } +} +/// A test context to be used in normal #[test] tests. +pub struct BlockingTestContext { + ctx: Option, + runtime: tokio::runtime::Runtime, +} + +impl BlockingTestContext { + pub fn new() -> Result { + TestContext::builder().build_blocking() + } + + pub const fn ctx(&self) -> &TestContext { + self.ctx.as_ref().expect("TestContext is set") + } + + pub const fn runtime(&self) -> &tokio::runtime::Runtime { + &self.runtime + } + + pub fn block_on<'ctx, F>(&'ctx self, f: impl FnOnce(&'ctx TestContext) -> F) -> F::Output + where + F: std::future::Future + 'ctx, + { + let future = f(self.ctx()); + self.runtime.block_on(future) + } + + pub fn block_on_mut<'ctx, F>( + &'ctx mut self, + f: impl FnOnce(&'ctx mut TestContext) -> F, + ) -> F::Output + where + F: std::future::Future + 'ctx, + { + let ctx_mut = self.ctx.as_mut().expect("TestContext is set"); + let future = f(ctx_mut); + self.runtime.block_on(future) } } @@ -290,13 +390,13 @@ impl Drop for BlockingTestContext { } #[must_use] -pub fn format_public_account_id(account_id: AccountId) -> String { - format!("Public/{account_id}") +pub const fn public_mention(account_id: AccountId) -> CliAccountMention { + CliAccountMention::Id(AccountIdWithPrivacy::Public(account_id)) } #[must_use] -pub fn format_private_account_id(account_id: AccountId) -> String { - format!("Private/{account_id}") +pub const fn private_mention(account_id: AccountId) -> CliAccountMention { + CliAccountMention::Id(AccountIdWithPrivacy::Private(account_id)) } #[expect( diff --git a/integration_tests/src/setup.rs b/integration_tests/src/setup.rs index 774c67e3..55c24e92 100644 --- a/integration_tests/src/setup.rs +++ b/integration_tests/src/setup.rs @@ -1,27 +1,30 @@ -use std::{ - ffi::{CString, c_char}, - fs::File, - io::Write as _, - net::SocketAddr, - path::PathBuf, -}; +use std::{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 nssa::PrivateKey; +use sequencer_service::{GenesisTransaction, SequencerHandle}; use tempfile::TempDir; use testcontainers::compose::DockerCompose; -use wallet::{WalletCore, config::WalletConfigOverrides}; +use wallet::{ + WalletCore, + cli::{ + Command, SubcommandReturnValue, + account::{AccountSubcommand, NewSubcommand}, + execute_subcommand, + programs::native_token_transfer::AuthTransferSubcommand, + }, + config::WalletConfigOverrides, +}; -use crate::{BEDROCK_SERVICE_PORT, BEDROCK_SERVICE_WITH_OPEN_PORT, config}; +use crate::{ + BEDROCK_SERVICE_PORT, BEDROCK_SERVICE_WITH_OPEN_PORT, + config::{self, INITIAL_PRIVATE_BALANCES_FOR_WALLET}, + private_mention, public_mention, +}; -unsafe extern "C" { - fn start_indexer(config_path: *const c_char, port: u16) -> InitializedIndexerServiceFFIResult; -} - -pub(crate) async fn setup_bedrock_node() -> Result<(DockerCompose, SocketAddr)> { +pub 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"); @@ -91,10 +94,7 @@ pub(crate) async fn setup_bedrock_node() -> Result<(DockerCompose, SocketAddr)> Ok((compose, addr)) } -pub(crate) async fn setup_indexer( - bedrock_addr: SocketAddr, - initial_data: &config::InitialData, -) -> Result<(IndexerHandle, TempDir)> { +pub async fn setup_indexer(bedrock_addr: SocketAddr) -> Result<(IndexerHandle, TempDir)> { let temp_indexer_dir = tempfile::tempdir().context("Failed to create temp dir for indexer home")?; @@ -103,12 +103,8 @@ pub(crate) async fn setup_indexer( 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 indexer_config = config::indexer_config(bedrock_addr, temp_indexer_dir.path().to_owned()) + .context("Failed to create Indexer config")?; indexer_service::run_server(indexer_config, 0) .await @@ -116,10 +112,10 @@ pub(crate) async fn setup_indexer( .map(|handle| (handle, temp_indexer_dir)) } -pub(crate) async fn setup_sequencer( +pub async fn setup_sequencer( partial: config::SequencerPartialConfig, bedrock_addr: SocketAddr, - initial_data: &config::InitialData, + genesis_transactions: Vec, ) -> Result<(SequencerHandle, TempDir)> { let temp_sequencer_dir = tempfile::tempdir().context("Failed to create temp dir for sequencer home")?; @@ -133,7 +129,7 @@ pub(crate) async fn setup_sequencer( partial, temp_sequencer_dir.path().to_owned(), bedrock_addr, - initial_data, + genesis_transactions, ) .context("Failed to create Sequencer config")?; @@ -142,12 +138,11 @@ pub(crate) async fn setup_sequencer( Ok((sequencer_handle, temp_sequencer_dir)) } -pub(crate) async fn setup_wallet( +pub fn setup_wallet( sequencer_addr: SocketAddr, - initial_data: &config::InitialData, + initial_public_accounts: &[(PrivateKey, u128)], ) -> Result<(WalletCore, TempDir, String)> { - let config = config::wallet_config(sequencer_addr, initial_data) - .context("Failed to create Wallet config")?; + let config = config::wallet_config(sequencer_addr).context("Failed to create Wallet config")?; let config_serialized = serde_json::to_string_pretty(&config).context("Failed to serialize Wallet config")?; @@ -162,57 +157,94 @@ pub(crate) async fn setup_wallet( let config_overrides = WalletConfigOverrides::default(); let wallet_password = "test_pass".to_owned(); - let (wallet, _mnemonic) = WalletCore::new_init_storage( + let (mut wallet, _mnemonic) = WalletCore::new_init_storage( config_path, storage_path, Some(config_overrides), &wallet_password, ) .context("Failed to init wallet")?; + + for (private_key, _balance) in initial_public_accounts { + wallet + .storage_mut() + .key_chain_mut() + .add_imported_public_account(private_key.clone()); + } + 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); +pub async fn setup_private_accounts_with_initial_supply(wallet: &mut WalletCore) -> Result<()> { + for _ in INITIAL_PRIVATE_BALANCES_FOR_WALLET { + let result = execute_subcommand( + wallet, + Command::Account(AccountSubcommand::New(NewSubcommand::Private { + cci: None, + label: None, + })), + ) + .await + .context("Failed to create a private account")?; + let SubcommandReturnValue::RegisterAccount { account_id: _ } = result else { + bail!("Expected RegisterAccount return value when creating private account"); + }; } - Ok(( - // SAFETY: lib function ensures validity of value. - unsafe { std::ptr::read(res.value) }, - temp_indexer_dir, - )) + let public_account_ids: Vec<_> = wallet + .storage() + .key_chain() + .public_account_ids() + .map(|(account_id, _idx)| account_id) + .collect(); + + if public_account_ids.len() < INITIAL_PRIVATE_BALANCES_FOR_WALLET.len() { + bail!( + "Expected at least {} public accounts in wallet storage, found {}", + INITIAL_PRIVATE_BALANCES_FOR_WALLET.len(), + public_account_ids.len() + ); + } + + let private_account_ids: Vec<_> = wallet + .storage() + .key_chain() + .private_account_ids() + .map(|(account_id, _idx)| account_id) + .collect(); + + for ((from, to), amount) in public_account_ids + .into_iter() + .zip(private_account_ids.into_iter()) + .zip(INITIAL_PRIVATE_BALANCES_FOR_WALLET) + { + let result = execute_subcommand( + wallet, + Command::AuthTransfer(AuthTransferSubcommand::Send { + from: public_mention(from), + to: Some(private_mention(to)), + to_npk: None, + to_vpk: None, + to_identifier: None, + amount, + }), + ) + .await + .context("Failed to perform initial shielded transfer to private account")?; + + if !matches!( + result, + SubcommandReturnValue::PrivacyPreservingTransfer { .. } + ) { + bail!( + "Expected PrivacyPreservingTransfer return value when shielding initial private funds" + ); + } + } + + Ok(()) } diff --git a/integration_tests/src/test_context_ffi.rs b/integration_tests/src/test_context_ffi.rs deleted file mode 100644 index d03a4e00..00000000 --- a/integration_tests/src/test_context_ffi.rs +++ /dev/null @@ -1,299 +0,0 @@ -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_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, - indexer_client::IndexerClient, - 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, - 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) - } - - #[must_use] - pub const fn indexer_ffi(&self) -> *const IndexerServiceFFI { - &raw const (self.indexer_ffi) - } -} - -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 47fda69f..82134840 100644 --- a/integration_tests/tests/account.rs +++ b/integration_tests/tests/account.rs @@ -3,16 +3,21 @@ reason = "We don't care about these in tests" )] -use anyhow::Result; -use integration_tests::{TestContext, format_private_account_id}; +use anyhow::{Context as _, Result}; +use integration_tests::{TestContext, private_mention}; +use key_protocol::key_management::KeyChain; use log::info; -use nssa::program::Program; +use nssa::{Data, program::Program}; +use nssa_core::account::Nonce; use sequencer_service_rpc::RpcClient as _; use tokio::test; -use wallet::cli::{ - Command, - account::{AccountSubcommand, NewSubcommand}, - execute_subcommand, +use wallet::{ + account::{AccountIdWithPrivacy, HumanReadableAccount, Label}, + cli::{ + Command, SubcommandReturnValue, + account::{AccountSubcommand, ImportSubcommand, NewSubcommand}, + execute_subcommand, + }, }; #[test] @@ -30,7 +35,7 @@ async fn get_existing_account() -> Result<()> { ); assert_eq!(account.balance, 10000); assert!(account.data.is_empty()); - assert_eq!(account.nonce.0, 0); + assert_eq!(account.nonce.0, 1); info!("Successfully retrieved account with correct details"); @@ -41,7 +46,7 @@ async fn get_existing_account() -> Result<()> { async fn new_public_account_with_label() -> Result<()> { let mut ctx = TestContext::new().await?; - let label = "my-test-public-account".to_owned(); + let label = Label::new("my-test-public-account"); let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public { cci: None, label: Some(label.clone()), @@ -55,14 +60,9 @@ async fn new_public_account_with_label() -> Result<()> { }; // 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"); + let resolved = ctx.wallet().storage().resolve_label(&label); - assert_eq!(stored_label.to_string(), label); + assert_eq!(resolved, Some(AccountIdWithPrivacy::Public(account_id))); info!("Successfully created public account with label"); @@ -74,23 +74,17 @@ 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 label = Label::new("my-test-private-account"); let command = Command::Account(AccountSubcommand::Label { - account_id: Some(format_private_account_id(account_id)), - account_label: None, + account_id: private_mention(account_id), label: label.clone(), }); execute_subcommand(ctx.wallet_mut(), command).await?; - let stored_label = ctx - .wallet() - .storage() - .labels - .get(&account_id.to_string()) - .expect("Label should be stored for the account"); + let resolved = ctx.wallet().storage().resolve_label(&label); - assert_eq!(stored_label.to_string(), label); + assert_eq!(resolved, Some(AccountIdWithPrivacy::Private(account_id))); info!("Successfully set label on existing private account"); @@ -114,12 +108,13 @@ async fn new_public_account_without_label() -> Result<()> { panic!("Expected RegisterAccount return value") }; - // Verify no label was stored + // Verify no label was stored for the account id assert!( - !ctx.wallet() + ctx.wallet() .storage() - .labels - .contains_key(&account_id.to_string()), + .labels_for_account(AccountIdWithPrivacy::Public(account_id)) + .next() + .is_none(), "No label should be stored when not provided" ); @@ -127,3 +122,150 @@ async fn new_public_account_without_label() -> Result<()> { Ok(()) } + +#[test] +async fn import_public_account() -> Result<()> { + let mut ctx = TestContext::new().await?; + + let private_key = nssa::PrivateKey::new_os_random(); + let account_id = nssa::AccountId::from(&nssa::PublicKey::new_from_private_key(&private_key)); + + let command = Command::Account(AccountSubcommand::Import(ImportSubcommand::Public { + private_key, + })); + let sub_ret = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + let SubcommandReturnValue::Empty = sub_ret else { + anyhow::bail!("Expected Empty return value"); + }; + + let imported_key = ctx + .wallet() + .storage() + .key_chain() + .pub_account_signing_key(account_id); + assert!( + imported_key.is_some(), + "Imported public account should be present" + ); + + Ok(()) +} + +#[test] +async fn import_private_account() -> Result<()> { + let mut ctx = TestContext::new().await?; + + let key_chain = KeyChain::new_os_random(); + let account_id = nssa::AccountId::from((&key_chain.nullifier_public_key, 0)); + let account = nssa::Account { + program_owner: Program::authenticated_transfer_program().id(), + balance: 777, + data: Data::default(), + nonce: Nonce::default(), + }; + + let key_chain_json = serde_json::to_string(&key_chain) + .context("Failed to serialize key chain for private import")?; + let account_state = HumanReadableAccount::from(account.clone()); + + let command = Command::Account(AccountSubcommand::Import(ImportSubcommand::Private { + key_chain_json, + account_state, + chain_index: None, + identifier: 0, + })); + let sub_ret = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + let SubcommandReturnValue::Empty = sub_ret else { + anyhow::bail!("Expected Empty return value"); + }; + + let imported_acc = ctx + .wallet() + .storage() + .key_chain() + .private_account(account_id) + .context("Imported private account should be present")?; + + assert_eq!( + imported_acc.key_chain.secret_spending_key, + key_chain.secret_spending_key + ); + assert_eq!( + imported_acc.key_chain.nullifier_public_key, + key_chain.nullifier_public_key + ); + assert_eq!( + imported_acc.key_chain.viewing_public_key, + key_chain.viewing_public_key + ); + + assert_eq!(imported_acc.chain_index, None); + + assert_eq!(imported_acc.identifier, 0); + + assert_eq!(imported_acc.account, &account); + + Ok(()) +} + +#[test] +async fn import_private_account_second_time_overrides_account_data() -> Result<()> { + let mut ctx = TestContext::new().await?; + + let key_chain = KeyChain::new_os_random(); + let account_id = nssa::AccountId::from((&key_chain.nullifier_public_key, 0)); + let key_chain_json = + serde_json::to_string(&key_chain).context("Failed to serialize key chain")?; + + let initial_account = nssa::Account { + program_owner: Program::authenticated_transfer_program().id(), + balance: 100, + data: Data::default(), + nonce: Nonce::default(), + }; + + // First import + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::Import(ImportSubcommand::Private { + key_chain_json: key_chain_json.clone(), + account_state: HumanReadableAccount::from(initial_account), + chain_index: None, + identifier: 0, + })), + ) + .await?; + + let updated_account = nssa::Account { + program_owner: Program::authenticated_transfer_program().id(), + balance: 999, + data: Data::default(), + nonce: Nonce::default(), + }; + + // Second import with different account data (same key chain) + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::Import(ImportSubcommand::Private { + key_chain_json, + account_state: HumanReadableAccount::from(updated_account.clone()), + chain_index: None, + identifier: 0, + })), + ) + .await?; + + let imported = ctx + .wallet() + .storage() + .key_chain() + .private_account(account_id) + .context("Imported private account should be present")?; + + assert_eq!( + imported.account, &updated_account, + "Second import should override account data" + ); + + Ok(()) +} diff --git a/integration_tests/tests/amm.rs b/integration_tests/tests/amm.rs index 3eaf35e2..b7a747f1 100644 --- a/integration_tests/tests/amm.rs +++ b/integration_tests/tests/amm.rs @@ -7,14 +7,17 @@ use std::time::Duration; use anyhow::Result; -use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, format_public_account_id}; +use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, public_mention}; use log::info; use sequencer_service_rpc::RpcClient as _; use tokio::test; -use wallet::cli::{ - Command, SubcommandReturnValue, - account::{AccountSubcommand, NewSubcommand}, - programs::{amm::AmmProgramAgnosticSubcommand, token::TokenProgramAgnosticSubcommand}, +use wallet::{ + account::Label, + cli::{ + Command, SubcommandReturnValue, + account::{AccountSubcommand, NewSubcommand}, + programs::{amm::AmmProgramAgnosticSubcommand, token::TokenProgramAgnosticSubcommand}, + }, }; #[test] @@ -113,10 +116,8 @@ async fn amm_public() -> Result<()> { // Create new token let subcommand = TokenProgramAgnosticSubcommand::New { - definition_account_id: Some(format_public_account_id(definition_account_id_1)), - definition_account_label: None, - supply_account_id: Some(format_public_account_id(supply_account_id_1)), - supply_account_label: None, + definition_account_id: public_mention(definition_account_id_1), + supply_account_id: public_mention(supply_account_id_1), name: "A NAM1".to_owned(), total_supply: 37, @@ -127,10 +128,8 @@ async fn amm_public() -> Result<()> { // Transfer 7 tokens from `supply_acc` to the account at account_id `recipient_account_id_1` let subcommand = TokenProgramAgnosticSubcommand::Send { - from: Some(format_public_account_id(supply_account_id_1)), - from_label: None, - to: Some(format_public_account_id(recipient_account_id_1)), - to_label: None, + from: public_mention(supply_account_id_1), + to: Some(public_mention(recipient_account_id_1)), to_npk: None, to_vpk: None, to_identifier: Some(0), @@ -143,10 +142,8 @@ async fn amm_public() -> Result<()> { // Create new token let subcommand = TokenProgramAgnosticSubcommand::New { - definition_account_id: Some(format_public_account_id(definition_account_id_2)), - definition_account_label: None, - supply_account_id: Some(format_public_account_id(supply_account_id_2)), - supply_account_label: None, + definition_account_id: public_mention(definition_account_id_2), + supply_account_id: public_mention(supply_account_id_2), name: "A NAM2".to_owned(), total_supply: 37, @@ -157,10 +154,8 @@ async fn amm_public() -> Result<()> { // Transfer 7 tokens from `supply_acc` to the account at account_id `recipient_account_id_2` let subcommand = TokenProgramAgnosticSubcommand::Send { - from: Some(format_public_account_id(supply_account_id_2)), - from_label: None, - to: Some(format_public_account_id(recipient_account_id_2)), - to_label: None, + from: public_mention(supply_account_id_2), + to: Some(public_mention(recipient_account_id_2)), to_npk: None, to_vpk: None, to_identifier: Some(0), @@ -193,12 +188,9 @@ async fn amm_public() -> Result<()> { // Send creation tx let subcommand = AmmProgramAgnosticSubcommand::New { - user_holding_a: Some(format_public_account_id(recipient_account_id_1)), - user_holding_a_label: None, - user_holding_b: Some(format_public_account_id(recipient_account_id_2)), - user_holding_b_label: None, - user_holding_lp: Some(format_public_account_id(user_holding_lp)), - user_holding_lp_label: None, + user_holding_a: public_mention(recipient_account_id_1), + user_holding_b: public_mention(recipient_account_id_2), + user_holding_lp: public_mention(user_holding_lp), balance_a: 3, balance_b: 3, }; @@ -239,13 +231,11 @@ async fn amm_public() -> Result<()> { // Make swap let subcommand = AmmProgramAgnosticSubcommand::SwapExactInput { - user_holding_a: Some(format_public_account_id(recipient_account_id_1)), - user_holding_a_label: None, - user_holding_b: Some(format_public_account_id(recipient_account_id_2)), - user_holding_b_label: None, + user_holding_a: public_mention(recipient_account_id_1), + user_holding_b: public_mention(recipient_account_id_2), amount_in: 2, min_amount_out: 1, - token_definition: definition_account_id_1.to_string(), + token_definition: definition_account_id_1, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::AMM(subcommand)).await?; @@ -284,13 +274,11 @@ async fn amm_public() -> Result<()> { // Make swap let subcommand = AmmProgramAgnosticSubcommand::SwapExactInput { - user_holding_a: Some(format_public_account_id(recipient_account_id_1)), - user_holding_a_label: None, - user_holding_b: Some(format_public_account_id(recipient_account_id_2)), - user_holding_b_label: None, + user_holding_a: public_mention(recipient_account_id_1), + user_holding_b: public_mention(recipient_account_id_2), amount_in: 2, min_amount_out: 1, - token_definition: definition_account_id_2.to_string(), + token_definition: definition_account_id_2, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::AMM(subcommand)).await?; @@ -329,12 +317,9 @@ async fn amm_public() -> Result<()> { // Add liquidity let subcommand = AmmProgramAgnosticSubcommand::AddLiquidity { - user_holding_a: Some(format_public_account_id(recipient_account_id_1)), - user_holding_a_label: None, - user_holding_b: Some(format_public_account_id(recipient_account_id_2)), - user_holding_b_label: None, - user_holding_lp: Some(format_public_account_id(user_holding_lp)), - user_holding_lp_label: None, + user_holding_a: public_mention(recipient_account_id_1), + user_holding_b: public_mention(recipient_account_id_2), + user_holding_lp: public_mention(user_holding_lp), min_amount_lp: 1, max_amount_a: 2, max_amount_b: 2, @@ -376,12 +361,9 @@ async fn amm_public() -> Result<()> { // Remove liquidity let subcommand = AmmProgramAgnosticSubcommand::RemoveLiquidity { - user_holding_a: Some(format_public_account_id(recipient_account_id_1)), - user_holding_a_label: None, - user_holding_b: Some(format_public_account_id(recipient_account_id_2)), - user_holding_b_label: None, - user_holding_lp: Some(format_public_account_id(user_holding_lp)), - user_holding_lp_label: None, + user_holding_a: public_mention(recipient_account_id_1), + user_holding_b: public_mention(recipient_account_id_2), + user_holding_lp: public_mention(user_holding_lp), balance_lp: 2, min_amount_a: 1, min_amount_b: 1, @@ -457,14 +439,14 @@ async fn amm_new_pool_using_labels() -> Result<()> { }; // Create holding_a with a label - let holding_a_label = "amm-holding-a-label".to_owned(); + let holding_a_label = Label::new("amm-holding-a-label"); let SubcommandReturnValue::RegisterAccount { account_id: holding_a_id, } = wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Account(AccountSubcommand::New(NewSubcommand::Public { cci: None, - label: Some(holding_a_label.clone()), + label: Some(Label::new(holding_a_label.clone())), })), ) .await? @@ -502,14 +484,14 @@ async fn amm_new_pool_using_labels() -> Result<()> { }; // Create holding_b with a label - let holding_b_label = "amm-holding-b-label".to_owned(); + let holding_b_label = Label::new("amm-holding-b-label"); let SubcommandReturnValue::RegisterAccount { account_id: holding_b_id, } = wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Account(AccountSubcommand::New(NewSubcommand::Public { cci: None, - label: Some(holding_b_label.clone()), + label: Some(Label::new(holding_b_label.clone())), })), ) .await? @@ -518,14 +500,14 @@ async fn amm_new_pool_using_labels() -> Result<()> { }; // Create holding_lp with a label - let holding_lp_label = "amm-holding-lp-label".to_owned(); + let holding_lp_label = Label::new("amm-holding-lp-label"); let SubcommandReturnValue::RegisterAccount { account_id: holding_lp_id, } = wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Account(AccountSubcommand::New(NewSubcommand::Public { cci: None, - label: Some(holding_lp_label.clone()), + label: Some(Label::new(holding_lp_label.clone())), })), ) .await? @@ -535,10 +517,8 @@ async fn amm_new_pool_using_labels() -> Result<()> { // Create token 1 and distribute to holding_a let subcommand = TokenProgramAgnosticSubcommand::New { - definition_account_id: Some(format_public_account_id(definition_account_id_1)), - definition_account_label: None, - supply_account_id: Some(format_public_account_id(supply_account_id_1)), - supply_account_label: None, + definition_account_id: public_mention(definition_account_id_1), + supply_account_id: public_mention(supply_account_id_1), name: "TOKEN1".to_owned(), total_supply: 10, }; @@ -546,10 +526,8 @@ async fn amm_new_pool_using_labels() -> Result<()> { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; let subcommand = TokenProgramAgnosticSubcommand::Send { - from: Some(format_public_account_id(supply_account_id_1)), - from_label: None, - to: Some(format_public_account_id(holding_a_id)), - to_label: None, + from: public_mention(supply_account_id_1), + to: Some(public_mention(holding_a_id)), to_npk: None, to_vpk: None, to_identifier: Some(0), @@ -560,10 +538,8 @@ async fn amm_new_pool_using_labels() -> Result<()> { // Create token 2 and distribute to holding_b let subcommand = TokenProgramAgnosticSubcommand::New { - definition_account_id: Some(format_public_account_id(definition_account_id_2)), - definition_account_label: None, - supply_account_id: Some(format_public_account_id(supply_account_id_2)), - supply_account_label: None, + definition_account_id: public_mention(definition_account_id_2), + supply_account_id: public_mention(supply_account_id_2), name: "TOKEN2".to_owned(), total_supply: 10, }; @@ -571,10 +547,8 @@ async fn amm_new_pool_using_labels() -> Result<()> { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; let subcommand = TokenProgramAgnosticSubcommand::Send { - from: Some(format_public_account_id(supply_account_id_2)), - from_label: None, - to: Some(format_public_account_id(holding_b_id)), - to_label: None, + from: public_mention(supply_account_id_2), + to: Some(public_mention(holding_b_id)), to_npk: None, to_vpk: None, to_identifier: Some(0), @@ -585,12 +559,9 @@ async fn amm_new_pool_using_labels() -> Result<()> { // Create AMM pool using account labels instead of IDs let subcommand = AmmProgramAgnosticSubcommand::New { - user_holding_a: None, - user_holding_a_label: Some(holding_a_label), - user_holding_b: None, - user_holding_b_label: Some(holding_b_label), - user_holding_lp: None, - user_holding_lp_label: Some(holding_lp_label), + user_holding_a: holding_a_label.into(), + user_holding_b: holding_b_label.into(), + user_holding_lp: holding_lp_label.into(), balance_a: 3, balance_b: 3, }; diff --git a/integration_tests/tests/ata.rs b/integration_tests/tests/ata.rs index 6f0bf05c..d0eddeae 100644 --- a/integration_tests/tests/ata.rs +++ b/integration_tests/tests/ata.rs @@ -9,8 +9,8 @@ use std::time::Duration; use anyhow::{Context as _, Result}; use ata_core::{compute_ata_seed, get_associated_token_account_id}; 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, private_mention, public_mention, + verify_commitment_is_in_state, }; use log::info; use nssa::program::Program; @@ -68,10 +68,8 @@ async fn create_ata_initializes_holding_account() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Token(TokenProgramAgnosticSubcommand::New { - definition_account_id: Some(format_public_account_id(definition_account_id)), - definition_account_label: None, - supply_account_id: Some(format_public_account_id(supply_account_id)), - supply_account_label: None, + definition_account_id: public_mention(definition_account_id), + supply_account_id: public_mention(supply_account_id), name: "TEST".to_owned(), total_supply, }), @@ -85,8 +83,8 @@ async fn create_ata_initializes_holding_account() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Ata(AtaSubcommand::Create { - owner: format_public_account_id(owner_account_id), - token_definition: definition_account_id.to_string(), + owner: public_mention(owner_account_id), + token_definition: definition_account_id, }), ) .await?; @@ -132,10 +130,8 @@ async fn create_ata_is_idempotent() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Token(TokenProgramAgnosticSubcommand::New { - definition_account_id: Some(format_public_account_id(definition_account_id)), - definition_account_label: None, - supply_account_id: Some(format_public_account_id(supply_account_id)), - supply_account_label: None, + definition_account_id: public_mention(definition_account_id), + supply_account_id: public_mention(supply_account_id), name: "TEST".to_owned(), total_supply: 100, }), @@ -149,8 +145,8 @@ async fn create_ata_is_idempotent() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Ata(AtaSubcommand::Create { - owner: format_public_account_id(owner_account_id), - token_definition: definition_account_id.to_string(), + owner: public_mention(owner_account_id), + token_definition: definition_account_id, }), ) .await?; @@ -162,8 +158,8 @@ async fn create_ata_is_idempotent() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Ata(AtaSubcommand::Create { - owner: format_public_account_id(owner_account_id), - token_definition: definition_account_id.to_string(), + owner: public_mention(owner_account_id), + token_definition: definition_account_id, }), ) .await?; @@ -212,10 +208,8 @@ async fn transfer_and_burn_via_ata() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Token(TokenProgramAgnosticSubcommand::New { - definition_account_id: Some(format_public_account_id(definition_account_id)), - definition_account_label: None, - supply_account_id: Some(format_public_account_id(supply_account_id)), - supply_account_label: None, + definition_account_id: public_mention(definition_account_id), + supply_account_id: public_mention(supply_account_id), name: "TEST".to_owned(), total_supply, }), @@ -240,16 +234,16 @@ async fn transfer_and_burn_via_ata() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Ata(AtaSubcommand::Create { - owner: format_public_account_id(sender_account_id), - token_definition: definition_account_id.to_string(), + owner: public_mention(sender_account_id), + token_definition: definition_account_id, }), ) .await?; wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Ata(AtaSubcommand::Create { - owner: format_public_account_id(recipient_account_id), - token_definition: definition_account_id.to_string(), + owner: public_mention(recipient_account_id), + token_definition: definition_account_id, }), ) .await?; @@ -262,10 +256,8 @@ async fn transfer_and_burn_via_ata() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Token(TokenProgramAgnosticSubcommand::Send { - from: Some(format_public_account_id(supply_account_id)), - from_label: None, - to: Some(format_public_account_id(sender_ata_id)), - to_label: None, + from: public_mention(supply_account_id), + to: Some(public_mention(sender_ata_id)), to_npk: None, to_vpk: None, to_identifier: Some(0), @@ -282,9 +274,9 @@ async fn transfer_and_burn_via_ata() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Ata(AtaSubcommand::Send { - from: format_public_account_id(sender_account_id), - token_definition: definition_account_id.to_string(), - to: recipient_ata_id.to_string(), + from: public_mention(sender_account_id), + token_definition: definition_account_id, + to: recipient_ata_id, amount: transfer_amount, }), ) @@ -320,8 +312,8 @@ async fn transfer_and_burn_via_ata() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Ata(AtaSubcommand::Burn { - holder: format_public_account_id(sender_account_id), - token_definition: definition_account_id.to_string(), + holder: public_mention(sender_account_id), + token_definition: definition_account_id, amount: burn_amount, }), ) @@ -371,10 +363,8 @@ async fn create_ata_with_private_owner() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Token(TokenProgramAgnosticSubcommand::New { - definition_account_id: Some(format_public_account_id(definition_account_id)), - definition_account_label: None, - supply_account_id: Some(format_public_account_id(supply_account_id)), - supply_account_label: None, + definition_account_id: public_mention(definition_account_id), + supply_account_id: public_mention(supply_account_id), name: "TEST".to_owned(), total_supply: 100, }), @@ -388,8 +378,8 @@ async fn create_ata_with_private_owner() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Ata(AtaSubcommand::Create { - owner: format_private_account_id(owner_account_id), - token_definition: definition_account_id.to_string(), + owner: private_mention(owner_account_id), + token_definition: definition_account_id, }), ) .await?; @@ -445,10 +435,8 @@ async fn transfer_via_ata_private_owner() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Token(TokenProgramAgnosticSubcommand::New { - definition_account_id: Some(format_public_account_id(definition_account_id)), - definition_account_label: None, - supply_account_id: Some(format_public_account_id(supply_account_id)), - supply_account_label: None, + definition_account_id: public_mention(definition_account_id), + supply_account_id: public_mention(supply_account_id), name: "TEST".to_owned(), total_supply, }), @@ -473,16 +461,16 @@ async fn transfer_via_ata_private_owner() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Ata(AtaSubcommand::Create { - owner: format_private_account_id(sender_account_id), - token_definition: definition_account_id.to_string(), + owner: private_mention(sender_account_id), + token_definition: definition_account_id, }), ) .await?; wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Ata(AtaSubcommand::Create { - owner: format_public_account_id(recipient_account_id), - token_definition: definition_account_id.to_string(), + owner: public_mention(recipient_account_id), + token_definition: definition_account_id, }), ) .await?; @@ -495,10 +483,8 @@ async fn transfer_via_ata_private_owner() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Token(TokenProgramAgnosticSubcommand::Send { - from: Some(format_public_account_id(supply_account_id)), - from_label: None, - to: Some(format_public_account_id(sender_ata_id)), - to_label: None, + from: public_mention(supply_account_id), + to: Some(public_mention(sender_ata_id)), to_npk: None, to_vpk: None, to_identifier: Some(0), @@ -515,9 +501,9 @@ async fn transfer_via_ata_private_owner() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Ata(AtaSubcommand::Send { - from: format_private_account_id(sender_account_id), - token_definition: definition_account_id.to_string(), - to: recipient_ata_id.to_string(), + from: private_mention(sender_account_id), + token_definition: definition_account_id, + to: recipient_ata_id, amount: transfer_amount, }), ) @@ -572,10 +558,8 @@ async fn burn_via_ata_private_owner() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Token(TokenProgramAgnosticSubcommand::New { - definition_account_id: Some(format_public_account_id(definition_account_id)), - definition_account_label: None, - supply_account_id: Some(format_public_account_id(supply_account_id)), - supply_account_label: None, + definition_account_id: public_mention(definition_account_id), + supply_account_id: public_mention(supply_account_id), name: "TEST".to_owned(), total_supply, }), @@ -596,8 +580,8 @@ async fn burn_via_ata_private_owner() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Ata(AtaSubcommand::Create { - owner: format_private_account_id(holder_account_id), - token_definition: definition_account_id.to_string(), + owner: private_mention(holder_account_id), + token_definition: definition_account_id, }), ) .await?; @@ -610,10 +594,8 @@ async fn burn_via_ata_private_owner() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Token(TokenProgramAgnosticSubcommand::Send { - from: Some(format_public_account_id(supply_account_id)), - from_label: None, - to: Some(format_public_account_id(holder_ata_id)), - to_label: None, + from: public_mention(supply_account_id), + to: Some(public_mention(holder_ata_id)), to_npk: None, to_vpk: None, to_identifier: Some(0), @@ -630,8 +612,8 @@ async fn burn_via_ata_private_owner() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Ata(AtaSubcommand::Burn { - holder: format_private_account_id(holder_account_id), - token_definition: definition_account_id.to_string(), + holder: private_mention(holder_account_id), + token_definition: definition_account_id, amount: burn_amount, }), ) diff --git a/integration_tests/tests/auth_transfer/private.rs b/integration_tests/tests/auth_transfer/private.rs index 8db5f8d4..31a1c361 100644 --- a/integration_tests/tests/auth_transfer/private.rs +++ b/integration_tests/tests/auth_transfer/private.rs @@ -2,18 +2,21 @@ use std::time::Duration; use anyhow::{Context as _, Result}; use integration_tests::{ - TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, fetch_privacy_preserving_tx, - format_private_account_id, format_public_account_id, verify_commitment_is_in_state, + TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, fetch_privacy_preserving_tx, private_mention, + public_mention, verify_commitment_is_in_state, }; use log::info; use nssa::{AccountId, program::Program}; use nssa_core::{NullifierPublicKey, encryption::shared_key_derivation::Secp256k1Point}; use sequencer_service_rpc::RpcClient as _; use tokio::test; -use wallet::cli::{ - Command, SubcommandReturnValue, - account::{AccountSubcommand, NewSubcommand}, - programs::native_token_transfer::AuthTransferSubcommand, +use wallet::{ + account::Label, + cli::{ + CliAccountMention, Command, SubcommandReturnValue, + account::{AccountSubcommand, NewSubcommand}, + programs::native_token_transfer::AuthTransferSubcommand, + }, }; #[test] @@ -24,10 +27,8 @@ async fn private_transfer_to_owned_account() -> Result<()> { 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, + from: private_mention(from), + to: Some(private_mention(to)), to_npk: None, to_vpk: None, to_identifier: Some(0), @@ -66,10 +67,8 @@ async fn private_transfer_to_foreign_account() -> Result<()> { let to_vpk = Secp256k1Point::from_scalar(to_npk.0); let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: Some(format_private_account_id(from)), - from_label: None, + from: private_mention(from), to: None, - to_label: None, to_npk: Some(to_npk_string), to_vpk: Some(hex::encode(to_vpk.0)), to_identifier: Some(0), @@ -117,10 +116,8 @@ async fn deshielded_transfer_to_public_account() -> Result<()> { assert_eq!(from_acc.balance, 10000); let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: Some(format_private_account_id(from)), - from_label: None, - to: Some(format_public_account_id(to)), - to_label: None, + from: private_mention(from), + to: Some(public_mention(to)), to_npk: None, to_vpk: None, to_identifier: Some(0), @@ -173,22 +170,20 @@ async fn private_transfer_to_owned_account_using_claiming_path() -> Result<()> { }; // Get the keys for the newly created account - let (to_keys, _, to_identifier) = ctx + let to = ctx .wallet() .storage() - .user_data - .get_private_account(to_account_id) + .key_chain() + .private_account(to_account_id) .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: Some(format_private_account_id(from)), - from_label: None, + from: private_mention(from), to: None, - 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), + to_npk: Some(hex::encode(to.key_chain.nullifier_public_key.0)), + to_vpk: Some(hex::encode(&to.key_chain.viewing_public_key.0)), + to_identifier: Some(to.identifier), amount: 100, }); @@ -233,10 +228,8 @@ async fn shielded_transfer_to_owned_private_account() -> Result<()> { let to: AccountId = ctx.existing_private_accounts()[1]; let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: Some(format_public_account_id(from)), - from_label: None, - to: Some(format_private_account_id(to)), - to_label: None, + from: public_mention(from), + to: Some(private_mention(to)), to_npk: None, to_vpk: None, to_identifier: Some(0), @@ -278,10 +271,8 @@ async fn shielded_transfer_to_foreign_account() -> Result<()> { let from: AccountId = ctx.existing_public_accounts()[0]; let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: Some(format_public_account_id(from)), - from_label: None, + from: public_mention(from), to: None, - to_label: None, to_npk: Some(to_npk_string), to_vpk: Some(hex::encode(to_vpk.0)), to_identifier: Some(0), @@ -341,22 +332,20 @@ async fn private_transfer_to_owned_account_continuous_run_path() -> Result<()> { }; // Get the newly created account's keys - let (to_keys, _, to_identifier) = ctx + let to = ctx .wallet() .storage() - .user_data - .get_private_account(to_account_id) + .key_chain() + .private_account(to_account_id) .context("Failed to get private account")?; // Send transfer using nullifier and viewing public keys let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: Some(format_private_account_id(from)), - from_label: None, + from: private_mention(from), to: None, - 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), + to_npk: Some(hex::encode(to.key_chain.nullifier_public_key.0)), + to_vpk: Some(hex::encode(&to.key_chain.viewing_public_key.0)), + to_identifier: Some(to.identifier), amount: 100, }); @@ -402,8 +391,7 @@ async fn initialize_private_account() -> Result<()> { }; let command = Command::AuthTransfer(AuthTransferSubcommand::Init { - account_id: Some(format_private_account_id(account_id)), - account_label: None, + account_id: private_mention(account_id), }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -444,20 +432,17 @@ async fn private_transfer_using_from_label() -> Result<()> { let to: AccountId = ctx.existing_private_accounts()[1]; // Assign a label to the sender account - let label = "private-sender-label".to_owned(); + let label = Label::new("private-sender-label"); let command = Command::Account(AccountSubcommand::Label { - account_id: Some(format_private_account_id(from)), - account_label: None, + account_id: private_mention(from), label: label.clone(), }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; // Send using the label instead of account ID let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: None, - from_label: Some(label), - to: Some(format_private_account_id(to)), - to_label: None, + from: CliAccountMention::Label(label), + to: Some(private_mention(to)), to_npk: None, to_vpk: None, to_identifier: Some(0), @@ -491,7 +476,7 @@ async fn initialize_private_account_using_label() -> Result<()> { let mut ctx = TestContext::new().await?; // Create a new private account with a label - let label = "init-private-label".to_owned(); + let label = Label::new("init-private-label"); let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private { cci: None, label: Some(label.clone()), @@ -503,8 +488,7 @@ async fn initialize_private_account_using_label() -> Result<()> { // Initialize using the label instead of account ID let command = Command::AuthTransfer(AuthTransferSubcommand::Init { - account_id: None, - account_label: Some(label), + account_id: label.into(), }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -541,15 +525,12 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> { // 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 + let key_chain = 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() + .private_account_key_chain_by_index(&chain_index) + .expect("Failed to get private account key chain for chain index"); ( key_chain.nullifier_public_key, key_chain.viewing_public_key.clone(), @@ -568,10 +549,8 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::AuthTransfer(AuthTransferSubcommand::Send { - from: Some(format_public_account_id(sender_0)), - from_label: None, + from: public_mention(sender_0), to: None, - to_label: None, to_npk: Some(npk_hex.clone()), to_vpk: Some(vpk_hex.clone()), to_identifier: Some(identifier_1), @@ -583,10 +562,8 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::AuthTransfer(AuthTransferSubcommand::Send { - from: Some(format_public_account_id(sender_1)), - from_label: None, + from: public_mention(sender_1), to: None, - to_label: None, to_npk: Some(npk_hex), to_vpk: Some(vpk_hex), to_identifier: Some(identifier_2), @@ -620,21 +597,25 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> { 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")?; + let found_acc1 = ctx + .wallet() + .storage() + .key_chain() + .private_account(account_id_1) + .context("account_id_1 not found in key chain")?; + let found_acc2 = ctx + .wallet() + .storage() + .key_chain() + .private_account(account_id_2) + .context("account_id_2 not found in key chain")?; assert_eq!( - ci_1, ci_2, + found_acc1.chain_index, found_acc2.chain_index, "identifiers 1 and 2 under the same NPK must share a single chain_index" ); assert_eq!( - ci_1, &chain_index, + found_acc1.chain_index, + Some(chain_index), "both accounts must resolve to the key node created at the start of the test" ); diff --git a/integration_tests/tests/auth_transfer/public.rs b/integration_tests/tests/auth_transfer/public.rs index e2b5a618..2b6ec130 100644 --- a/integration_tests/tests/auth_transfer/public.rs +++ b/integration_tests/tests/auth_transfer/public.rs @@ -1,15 +1,18 @@ use std::time::Duration; use anyhow::Result; -use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, format_public_account_id}; +use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, public_mention}; use log::info; use nssa::program::Program; use sequencer_service_rpc::RpcClient as _; use tokio::test; -use wallet::cli::{ - Command, SubcommandReturnValue, - account::{AccountSubcommand, NewSubcommand}, - programs::native_token_transfer::AuthTransferSubcommand, +use wallet::{ + account::Label, + cli::{ + CliAccountMention, Command, SubcommandReturnValue, + account::{AccountSubcommand, NewSubcommand}, + programs::native_token_transfer::AuthTransferSubcommand, + }, }; #[test] @@ -17,10 +20,8 @@ async fn successful_transfer_to_existing_account() -> Result<()> { let mut ctx = TestContext::new().await?; 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, + from: public_mention(ctx.existing_public_accounts()[0]), + to: Some(public_mention(ctx.existing_public_accounts()[1])), to_npk: None, to_vpk: None, to_identifier: Some(0), @@ -67,8 +68,9 @@ pub async fn successful_transfer_to_new_account() -> Result<()> { let new_persistent_account_id = ctx .wallet() .storage() - .user_data - .account_ids() + .key_chain() + .public_account_ids() + .map(|(account_id, _)| account_id) .find(|acc_id| { *acc_id != ctx.existing_public_accounts()[0] && *acc_id != ctx.existing_public_accounts()[1] @@ -76,10 +78,8 @@ pub async fn successful_transfer_to_new_account() -> Result<()> { .expect("Failed to find newly created account in the wallet storage"); 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(new_persistent_account_id)), - to_label: None, + from: public_mention(ctx.existing_public_accounts()[0]), + to: Some(public_mention(new_persistent_account_id)), to_npk: None, to_vpk: None, to_identifier: Some(0), @@ -115,10 +115,8 @@ async fn failed_transfer_with_insufficient_balance() -> Result<()> { let mut ctx = TestContext::new().await?; 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, + from: public_mention(ctx.existing_public_accounts()[0]), + to: Some(public_mention(ctx.existing_public_accounts()[1])), to_npk: None, to_vpk: None, to_identifier: Some(0), @@ -156,10 +154,8 @@ async fn two_consecutive_successful_transfers() -> Result<()> { // First transfer 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, + from: public_mention(ctx.existing_public_accounts()[0]), + to: Some(public_mention(ctx.existing_public_accounts()[1])), to_npk: None, to_vpk: None, to_identifier: Some(0), @@ -191,10 +187,8 @@ async fn two_consecutive_successful_transfers() -> Result<()> { // Second transfer 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, + from: public_mention(ctx.existing_public_accounts()[0]), + to: Some(public_mention(ctx.existing_public_accounts()[1])), to_npk: None, to_vpk: None, to_identifier: Some(0), @@ -241,8 +235,7 @@ async fn initialize_public_account() -> Result<()> { }; let command = Command::AuthTransfer(AuthTransferSubcommand::Init { - account_id: Some(format_public_account_id(account_id)), - account_label: None, + account_id: public_mention(account_id), }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -267,20 +260,17 @@ async fn successful_transfer_using_from_label() -> Result<()> { let mut ctx = TestContext::new().await?; // Assign a label to the sender account - let label = "sender-label".to_owned(); + let label = Label::new("sender-label"); let command = Command::Account(AccountSubcommand::Label { - account_id: Some(format_public_account_id(ctx.existing_public_accounts()[0])), - account_label: None, + account_id: public_mention(ctx.existing_public_accounts()[0]), label: label.clone(), }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; // Send using the label instead of account ID let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: None, - from_label: Some(label), - to: Some(format_public_account_id(ctx.existing_public_accounts()[1])), - to_label: None, + from: CliAccountMention::Label(label), + to: Some(public_mention(ctx.existing_public_accounts()[1])), to_npk: None, to_vpk: None, to_identifier: Some(0), @@ -315,20 +305,17 @@ async fn successful_transfer_using_to_label() -> Result<()> { let mut ctx = TestContext::new().await?; // Assign a label to the receiver account - let label = "receiver-label".to_owned(); + let label = Label::new("receiver-label"); let command = Command::Account(AccountSubcommand::Label { - account_id: Some(format_public_account_id(ctx.existing_public_accounts()[1])), - account_label: None, + account_id: public_mention(ctx.existing_public_accounts()[1]), label: label.clone(), }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; // Send using the label for the recipient let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: Some(format_public_account_id(ctx.existing_public_accounts()[0])), - from_label: None, - to: None, - to_label: Some(label), + from: public_mention(ctx.existing_public_accounts()[0]), + to: Some(CliAccountMention::Label(label)), to_npk: None, to_vpk: None, to_identifier: Some(0), diff --git a/integration_tests/tests/indexer.rs b/integration_tests/tests/indexer.rs index 21463117..5cf33cde 100644 --- a/integration_tests/tests/indexer.rs +++ b/integration_tests/tests/indexer.rs @@ -9,54 +9,61 @@ use std::time::Duration; use anyhow::{Context as _, Result}; use indexer_service_rpc::RpcClient as _; 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, private_mention, public_mention, + verify_commitment_is_in_state, }; use log::info; use nssa::AccountId; -use wallet::cli::{Command, programs::native_token_transfer::AuthTransferSubcommand}; +use wallet::{ + account::Label, + cli::{CliAccountMention, 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; /// Poll the indexer until its last finalized block id reaches the sequencer's -/// current last block id (and at least the genesis block has been advanced past), -/// or until [`L2_TO_L1_TIMEOUT_MILLIS`] elapses. Returns the last indexer block -/// id observed. -async fn wait_for_indexer_to_catch_up(ctx: &TestContext) -> u64 { +/// current last block id or until [`L2_TO_L1_TIMEOUT_MILLIS`] elapses. +/// Returns the last indexer block id observed. +async fn wait_for_indexer_to_catch_up(ctx: &TestContext) -> Result { let timeout = Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS); + let block_id_to_catch_up = + sequencer_service_rpc::RpcClient::get_last_block_id(ctx.sequencer_client()).await?; let mut last_ind: u64 = 1; let inner = async { loop { - let seq = sequencer_service_rpc::RpcClient::get_last_block_id(ctx.sequencer_client()) - .await - .unwrap_or(0); let ind = ctx .indexer_client() .get_last_finalized_block_id() - .await - .unwrap_or(1); + .await? + .unwrap_or(0); last_ind = ind; - if ind >= seq && ind > 1 { - info!("Indexer caught up: seq={seq}, ind={ind}"); - return ind; + if ind >= block_id_to_catch_up { + let last_seq = + sequencer_service_rpc::RpcClient::get_last_block_id(ctx.sequencer_client()) + .await?; + info!( + "Indexer caught up. Indexer last block id: {ind}. Current sequencer last block id: {last_seq}" + ); + return Ok(ind); } tokio::time::sleep(Duration::from_secs(2)).await; } }; tokio::time::timeout(timeout, inner) .await - .unwrap_or_else(|_| { - info!("Indexer catch-up timed out: ind={last_ind}"); - last_ind - }) + .with_context(|| { + format!( + "Indexer failed to catch up within {L2_TO_L1_TIMEOUT_MILLIS} milliseconds. Last indexer block id observed: {last_ind}, but needed to catch up to at least {block_id_to_catch_up}" + ) + })? } #[tokio::test] async fn indexer_test_run() -> Result<()> { let ctx = TestContext::new().await?; - let last_block_indexer = wait_for_indexer_to_catch_up(&ctx).await; + let last_block_indexer = wait_for_indexer_to_catch_up(&ctx).await?; let last_block_seq = sequencer_service_rpc::RpcClient::get_last_block_id(ctx.sequencer_client()).await?; @@ -64,7 +71,7 @@ async fn indexer_test_run() -> Result<()> { info!("Last block on seq now is {last_block_seq}"); info!("Last block on ind now is {last_block_indexer}"); - assert!(last_block_indexer > 1); + assert!(last_block_indexer > 0); Ok(()) } @@ -74,11 +81,11 @@ async fn indexer_block_batching() -> Result<()> { let ctx = TestContext::new().await?; info!("Waiting for indexer to parse blocks"); - let last_block_indexer = wait_for_indexer_to_catch_up(&ctx).await; + let last_block_indexer = wait_for_indexer_to_catch_up(&ctx).await?; info!("Last block on ind now is {last_block_indexer}"); - assert!(last_block_indexer > 1); + assert!(last_block_indexer > 0); // Getting wide batch to fit all blocks (from latest backwards) let mut block_batch = ctx.indexer_client().get_blocks(None, 100).await.unwrap(); @@ -105,10 +112,8 @@ async fn indexer_state_consistency() -> Result<()> { let mut ctx = TestContext::new().await?; 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, + from: public_mention(ctx.existing_public_accounts()[0]), + to: Some(public_mention(ctx.existing_public_accounts()[1])), to_npk: None, to_vpk: None, to_identifier: Some(0), @@ -142,10 +147,8 @@ async fn indexer_state_consistency() -> Result<()> { 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, + from: private_mention(from), + to: Some(private_mention(to)), to_npk: None, to_vpk: None, to_identifier: Some(0), @@ -172,7 +175,7 @@ async fn indexer_state_consistency() -> Result<()> { info!("Successfully transferred privately to owned account"); info!("Waiting for indexer to parse blocks"); - wait_for_indexer_to_catch_up(&ctx).await; + wait_for_indexer_to_catch_up(&ctx).await?; let acc1_ind_state = ctx .indexer_client() @@ -210,29 +213,25 @@ async fn indexer_state_consistency_with_labels() -> Result<()> { let mut ctx = TestContext::new().await?; // Assign labels to both accounts - let from_label = "idx-sender-label".to_owned(); - let to_label_str = "idx-receiver-label".to_owned(); + let from_label = Label::new("idx-sender-label"); + let to_label = Label::new("idx-receiver-label"); 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, + account_id: public_mention(ctx.existing_public_accounts()[0]), label: from_label.clone(), }); wallet::cli::execute_subcommand(ctx.wallet_mut(), label_cmd).await?; 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(), + account_id: public_mention(ctx.existing_public_accounts()[1]), + label: to_label.clone(), }); wallet::cli::execute_subcommand(ctx.wallet_mut(), label_cmd).await?; // 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), + from: CliAccountMention::Label(from_label), + to: Some(CliAccountMention::Label(to_label)), to_npk: None, to_vpk: None, to_identifier: Some(0), @@ -259,7 +258,7 @@ async fn indexer_state_consistency_with_labels() -> Result<()> { assert_eq!(acc_2_balance, 20100); info!("Waiting for indexer to parse blocks"); - wait_for_indexer_to_catch_up(&ctx).await; + wait_for_indexer_to_catch_up(&ctx).await?; let acc1_ind_state = ctx .indexer_client() diff --git a/integration_tests/tests/indexer_ffi.rs b/integration_tests/tests/indexer_ffi.rs index bbc329e3..c1523061 100644 --- a/integration_tests/tests/indexer_ffi.rs +++ b/integration_tests/tests/indexer_ffi.rs @@ -5,82 +5,145 @@ reason = "We don't care about these in tests" )] +use std::{ + ffi::{CString, c_char}, + fs::File, + io::Write as _, + net::SocketAddr, +}; + use anyhow::{Context as _, Result}; use indexer_ffi::{ - IndexerServiceFFI, OperationStatus, + IndexerServiceFFI, OperationStatus, Runtime, api::{ PointerResult, + lifecycle::InitializedIndexerServiceFFIResult, types::{FfiAccountId, FfiOption, FfiVec, account::FfiAccount, block::FfiBlock}, }, }; 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, + BlockingTestContext, TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, private_mention, + public_mention, verify_commitment_is_in_state, }; -use log::info; +use log::{debug, info}; use nssa::AccountId; -use wallet::cli::{Command, programs::native_token_transfer::AuthTransferSubcommand}; +use tempfile::TempDir; +use wallet::{ + account::Label, + 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; unsafe extern "C" { unsafe fn query_last_block( + runtime: *const Runtime, indexer: *const IndexerServiceFFI, ) -> PointerResult; unsafe fn query_block_vec( + runtime: *const Runtime, indexer: *const IndexerServiceFFI, before: FfiOption, limit: u64, ) -> PointerResult, OperationStatus>; unsafe fn query_account( + runtime: *const Runtime, indexer: *const IndexerServiceFFI, account_id: FfiAccountId, ) -> PointerResult; + + unsafe fn start_indexer( + runtime: *const Runtime, + config_path: *const c_char, + port: u16, + ) -> InitializedIndexerServiceFFIResult; +} + +fn setup_indexer_ffi( + runtime: &Runtime, + bedrock_addr: SocketAddr, +) -> 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 = + integration_tests::config::indexer_config(bedrock_addr, temp_indexer_dir.path().to_owned()) + .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(std::ptr::from_ref(runtime), 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, + )) +} + +/// Prepare setup for tests. +fn setup() -> Result<(BlockingTestContext, IndexerServiceFFI, TempDir)> { + let ctx = TestContext::builder().disable_indexer().build_blocking()?; + // Safety: ctx runtime is valid for the lifetime of the returned Runtime + let runtime = unsafe { Runtime::from_borrowed(ctx.runtime()) }; + let (indexer_ffi, indexer_dir) = setup_indexer_ffi(&runtime, ctx.ctx().bedrock_addr())?; + + Ok((ctx, indexer_ffi, indexer_dir)) } #[test] fn indexer_test_run_ffi() -> Result<()> { - let blocking_ctx = BlockingTestContextFFI::new()?; - let runtime_wrapped = blocking_ctx.runtime(); + let (ctx, indexer_ffi, _indexer_dir) = setup()?; // RUN OBSERVATION - runtime_wrapped.block_on(async { - tokio::time::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)).await; - }); + std::thread::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)); - let last_block_indexer = blocking_ctx.ctx().get_last_block_indexer(runtime_wrapped)?; - let last_block_indexer_ffi_res = unsafe { query_last_block(blocking_ctx.indexer_ffi()) }; + // Safety: ctx runtime is valid for the lifetime of the returned Runtime + let runtime = unsafe { Runtime::from_borrowed(ctx.runtime()) }; + let last_block_indexer_ffi_res = + unsafe { query_last_block(&raw const runtime, &raw const indexer_ffi) }; assert!(last_block_indexer_ffi_res.error.is_ok()); let last_block_indexer_ffi = unsafe { *last_block_indexer_ffi_res.value }; - info!("Last block on ind now is {last_block_indexer}"); info!("Last block on ind ffi now is {last_block_indexer_ffi}"); - assert!(last_block_indexer > 1); assert!(last_block_indexer_ffi > 1); - assert_eq!(last_block_indexer, last_block_indexer_ffi); - Ok(()) } #[test] fn indexer_ffi_block_batching() -> Result<()> { - let blocking_ctx = BlockingTestContextFFI::new()?; - let runtime_wrapped = blocking_ctx.runtime(); + let (ctx, indexer_ffi, _indexer_dir) = setup()?; // 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; - }); + std::thread::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)); - let last_block_indexer_ffi_res = unsafe { query_last_block(blocking_ctx.indexer_ffi()) }; + // Safety: ctx runtime is valid for the lifetime of the returned Runtime + let runtime = unsafe { Runtime::from_borrowed(ctx.runtime()) }; + let last_block_indexer_ffi_res = + unsafe { query_last_block(&raw const runtime, &raw const indexer_ffi) }; assert!(last_block_indexer_ffi_res.error.is_ok()); @@ -93,8 +156,14 @@ fn indexer_ffi_block_batching() -> Result<()> { let before_ffi = FfiOption::::from_none(); let limit = 100; - let block_batch_ffi_res = - unsafe { query_block_vec(blocking_ctx.indexer_ffi(), before_ffi, limit) }; + let block_batch_ffi_res = unsafe { + query_block_vec( + &raw const runtime, + &raw const indexer_ffi, + before_ffi, + limit, + ) + }; assert!(block_batch_ffi_res.error.is_ok()); @@ -117,43 +186,37 @@ fn indexer_ffi_block_batching() -> Result<()> { #[test] fn indexer_ffi_state_consistency() -> Result<()> { - let mut blocking_ctx = BlockingTestContextFFI::new()?; - let runtime_wrapped = blocking_ctx.runtime_clone(); - let indexer_ffi = blocking_ctx.indexer_ffi(); - let ctx = blocking_ctx.ctx_mut(); + let (mut ctx, indexer_ffi, _indexer_dir) = setup()?; 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, + from: public_mention(ctx.ctx().existing_public_accounts()[0]), + to: Some(public_mention(ctx.ctx().existing_public_accounts()[1])), to_npk: None, to_vpk: None, amount: 100, to_identifier: Some(0), }); - runtime_wrapped.block_on(wallet::cli::execute_subcommand(ctx.wallet_mut(), command))?; + ctx.block_on_mut(|ctx| 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; - }); + std::thread::sleep(std::time::Duration::from_secs( + TIME_TO_WAIT_FOR_BLOCK_SECONDS, + )); info!("Checking correct balance move"); - let acc_1_balance = - runtime_wrapped.block_on(sequencer_service_rpc::RpcClient::get_account_balance( + let acc_1_balance = ctx.block_on(|ctx| { + 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( + ) + })?; + let acc_2_balance = ctx.block_on(|ctx| { + 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:#?}"); @@ -161,68 +224,71 @@ fn indexer_ffi_state_consistency() -> Result<()> { 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 from: AccountId = ctx.ctx().existing_private_accounts()[0]; + let to: AccountId = ctx.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, + from: private_mention(from), + to: Some(private_mention(to)), to_npk: None, to_vpk: None, amount: 100, to_identifier: Some(0), }); - runtime_wrapped.block_on(wallet::cli::execute_subcommand(ctx.wallet_mut(), command))?; + ctx.block_on_mut(|ctx| 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; - }); + std::thread::sleep(std::time::Duration::from_secs( + TIME_TO_WAIT_FOR_BLOCK_SECONDS, + )); let new_commitment1 = ctx + .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(), - )); + let commitment_check1 = + ctx.block_on(|ctx| verify_commitment_is_in_state(new_commitment1, ctx.sequencer_client())); assert!(commitment_check1); let new_commitment2 = ctx + .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(), - )); + let commitment_check2 = + ctx.block_on(|ctx| 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; - }); + std::thread::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)); - let acc1_ind_state_ffi = - unsafe { query_account(indexer_ffi, (&ctx.existing_public_accounts()[0]).into()) }; + // Safety: ctx runtime is valid for the lifetime of the returned Runtime + let runtime = unsafe { Runtime::from_borrowed(ctx.runtime()) }; + let acc1_ind_state_ffi = unsafe { + query_account( + &raw const runtime, + &raw const indexer_ffi, + (&ctx.ctx().existing_public_accounts()[0]).into(), + ) + }; assert!(acc1_ind_state_ffi.error.is_ok()); let acc1_ind_state_pre = unsafe { &*acc1_ind_state_ffi.value }; let acc1_ind_state: indexer_service_protocol::Account = acc1_ind_state_pre.into(); - let acc2_ind_state_ffi = - unsafe { query_account(indexer_ffi, (&ctx.existing_public_accounts()[1]).into()) }; + let acc2_ind_state_ffi = unsafe { + query_account( + &raw const runtime, + &raw const indexer_ffi, + (&ctx.ctx().existing_public_accounts()[1]).into(), + ) + }; assert!(acc2_ind_state_ffi.error.is_ok()); @@ -230,16 +296,18 @@ fn indexer_ffi_state_consistency() -> Result<()> { let acc2_ind_state: indexer_service_protocol::Account = acc2_ind_state_pre.into(); info!("Checking correct state transition"); - let acc1_seq_state = - runtime_wrapped.block_on(sequencer_service_rpc::RpcClient::get_account( + let acc1_seq_state = ctx.block_on(|ctx| { + 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( + ) + })?; + let acc2_seq_state = ctx.block_on(|ctx| { + 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()); @@ -251,83 +319,81 @@ fn indexer_ffi_state_consistency() -> Result<()> { #[test] fn indexer_ffi_state_consistency_with_labels() -> Result<()> { - let mut blocking_ctx = BlockingTestContextFFI::new()?; - let runtime_wrapped = blocking_ctx.runtime_clone(); - let indexer_ffi = blocking_ctx.indexer_ffi(); - let ctx = blocking_ctx.ctx_mut(); + let (mut ctx, indexer_ffi, _indexer_dir) = setup()?; // Assign labels to both accounts - let from_label = "idx-sender-label".to_owned(); - let to_label_str = "idx-receiver-label".to_owned(); + let from_label = Label::new("idx-sender-label"); + let to_label = Label::new("idx-receiver-label"); 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, + account_id: public_mention(ctx.ctx().existing_public_accounts()[0]), label: from_label.clone(), }); - runtime_wrapped.block_on(wallet::cli::execute_subcommand(ctx.wallet_mut(), label_cmd))?; + ctx.block_on_mut(|ctx| 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(), + account_id: public_mention(ctx.ctx().existing_public_accounts()[1]), + label: to_label.clone(), }); - runtime_wrapped.block_on(wallet::cli::execute_subcommand(ctx.wallet_mut(), label_cmd))?; + ctx.block_on_mut(|ctx| 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), + from: from_label.into(), + to: Some(to_label.into()), to_npk: None, to_vpk: None, amount: 100, to_identifier: Some(0), }); - runtime_wrapped.block_on(wallet::cli::execute_subcommand(ctx.wallet_mut(), command))?; + ctx.block_on_mut(|ctx| 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; - }); + std::thread::sleep(std::time::Duration::from_secs( + TIME_TO_WAIT_FOR_BLOCK_SECONDS, + )); - let acc_1_balance = - runtime_wrapped.block_on(sequencer_service_rpc::RpcClient::get_account_balance( + let acc_1_balance = ctx.block_on(|ctx| { + 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( + ) + })?; + let acc_2_balance = ctx.block_on(|ctx| { + 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; - }); + std::thread::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)); - let acc1_ind_state_ffi = - unsafe { query_account(indexer_ffi, (&ctx.existing_public_accounts()[0]).into()) }; + // Safety: ctx runtime is valid for the lifetime of the returned Runtime + let runtime = unsafe { Runtime::from_borrowed(ctx.runtime()) }; + let acc1_ind_state_ffi = unsafe { + query_account( + &raw const runtime, + &raw const indexer_ffi, + (&ctx.ctx().existing_public_accounts()[0]).into(), + ) + }; assert!(acc1_ind_state_ffi.error.is_ok()); let acc1_ind_state_pre = unsafe { &*acc1_ind_state_ffi.value }; let acc1_ind_state: indexer_service_protocol::Account = acc1_ind_state_pre.into(); - let acc1_seq_state = - runtime_wrapped.block_on(sequencer_service_rpc::RpcClient::get_account( + let acc1_seq_state = ctx.block_on(|ctx| { + sequencer_service_rpc::RpcClient::get_account( ctx.sequencer_client(), ctx.existing_public_accounts()[0], - ))?; + ) + })?; assert_eq!(acc1_ind_state, acc1_seq_state.into()); diff --git a/integration_tests/tests/keys_restoration.rs b/integration_tests/tests/keys.rs similarity index 82% rename from integration_tests/tests/keys_restoration.rs rename to integration_tests/tests/keys.rs index ff339120..c49df396 100644 --- a/integration_tests/tests/keys_restoration.rs +++ b/integration_tests/tests/keys.rs @@ -8,8 +8,8 @@ use std::{str::FromStr as _, time::Duration}; use anyhow::{Context as _, Result}; use integration_tests::{ - TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, fetch_privacy_preserving_tx, - format_private_account_id, format_public_account_id, verify_commitment_is_in_state, + TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, fetch_privacy_preserving_tx, private_mention, + public_mention, verify_commitment_is_in_state, }; use key_protocol::key_management::key_tree::chain_index::ChainIndex; use log::info; @@ -59,22 +59,20 @@ async fn sync_private_account_with_non_zero_chain_index() -> Result<()> { }; // Get the keys for the newly created account - let (to_keys, _, to_identifier) = ctx + let to_account = ctx .wallet() .storage() - .user_data - .get_private_account(to_account_id) + .key_chain() + .private_account(to_account_id) .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: Some(format_private_account_id(from)), - from_label: None, + from: private_mention(from), to: None, - 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), + to_npk: Some(hex::encode(to_account.key_chain.nullifier_public_key.0)), + to_vpk: Some(hex::encode(&to_account.key_chain.viewing_public_key.0)), + to_identifier: Some(to_account.identifier), amount: 100, }); @@ -145,10 +143,8 @@ async fn restore_keys_from_seed() -> Result<()> { // Send to first private account let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: Some(format_private_account_id(from)), - from_label: None, - to: Some(format_private_account_id(to_account_id1)), - to_label: None, + from: private_mention(from), + to: Some(private_mention(to_account_id1)), to_npk: None, to_vpk: None, to_identifier: Some(0), @@ -158,10 +154,8 @@ async fn restore_keys_from_seed() -> Result<()> { // Send to second private account let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: Some(format_private_account_id(from)), - from_label: None, - to: Some(format_private_account_id(to_account_id2)), - to_label: None, + from: private_mention(from), + to: Some(private_mention(to_account_id2)), to_npk: None, to_vpk: None, to_identifier: Some(0), @@ -199,10 +193,8 @@ async fn restore_keys_from_seed() -> Result<()> { // Send to first public account let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: Some(format_public_account_id(from)), - from_label: None, - to: Some(format_public_account_id(to_account_id3)), - to_label: None, + from: public_mention(from), + to: Some(public_mention(to_account_id3)), to_npk: None, to_vpk: None, to_identifier: Some(0), @@ -212,10 +204,8 @@ async fn restore_keys_from_seed() -> Result<()> { // Send to second public account let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: Some(format_public_account_id(from)), - from_label: None, - to: Some(format_public_account_id(to_account_id4)), - to_label: None, + from: public_mention(from), + to: Some(public_mention(to_account_id4)), to_npk: None, to_vpk: None, to_identifier: Some(0), @@ -232,56 +222,50 @@ async fn restore_keys_from_seed() -> Result<()> { let acc1 = ctx .wallet() .storage() - .user_data - .private_key_tree - .get_node(to_account_id1) + .key_chain() + .private_account(to_account_id1) .expect("Acc 1 should be restored"); let acc2 = ctx .wallet() .storage() - .user_data - .private_key_tree - .get_node(to_account_id2) + .key_chain() + .private_account(to_account_id2) .expect("Acc 2 should be restored"); // Verify restored public accounts let _acc3 = ctx .wallet() .storage() - .user_data - .public_key_tree - .get_node(to_account_id3) + .key_chain() + .pub_account_signing_key(to_account_id3) .expect("Acc 3 should be restored"); let _acc4 = ctx .wallet() .storage() - .user_data - .public_key_tree - .get_node(to_account_id4) + .key_chain() + .pub_account_signing_key(to_account_id4) .expect("Acc 4 should be restored"); assert_eq!( - acc1.value.1[0].1.program_owner, + acc1.account.program_owner, Program::authenticated_transfer_program().id() ); assert_eq!( - acc2.value.1[0].1.program_owner, + acc2.account.program_owner, Program::authenticated_transfer_program().id() ); - assert_eq!(acc1.value.1[0].1.balance, 100); - assert_eq!(acc2.value.1[0].1.balance, 101); + assert_eq!(acc1.account.balance, 100); + assert_eq!(acc2.account.balance, 101); info!("Tree checks passed, testing restored accounts can transact"); // Test that restored accounts can send transactions let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: Some(format_private_account_id(to_account_id1)), - from_label: None, - to: Some(format_private_account_id(to_account_id2)), - to_label: None, + from: private_mention(to_account_id1), + to: Some(private_mention(to_account_id2)), to_npk: None, to_vpk: None, to_identifier: Some(0), @@ -290,10 +274,8 @@ async fn restore_keys_from_seed() -> Result<()> { wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: Some(format_public_account_id(to_account_id3)), - from_label: None, - to: Some(format_public_account_id(to_account_id4)), - to_label: None, + from: public_mention(to_account_id3), + to: Some(public_mention(to_account_id4)), to_npk: None, to_vpk: None, to_identifier: Some(0), diff --git a/integration_tests/tests/pinata.rs b/integration_tests/tests/pinata.rs index 77c4a646..9beb5b1f 100644 --- a/integration_tests/tests/pinata.rs +++ b/integration_tests/tests/pinata.rs @@ -9,8 +9,8 @@ use std::time::Duration; use anyhow::{Context as _, Result}; use common::PINATA_BASE58; 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, private_mention, public_mention, + verify_commitment_is_in_state, }; use log::info; use sequencer_service_rpc::RpcClient as _; @@ -42,8 +42,6 @@ async fn claim_pinata_to_uninitialized_public_account_fails_fast() -> Result<()> 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()) @@ -52,8 +50,7 @@ async fn claim_pinata_to_uninitialized_public_account_fails_fast() -> Result<()> let claim_result = wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Pinata(PinataProgramAgnosticSubcommand::Claim { - to: Some(winner_account_id_formatted), - to_label: None, + to: public_mention(winner_account_id), }), ) .await; @@ -97,8 +94,6 @@ async fn claim_pinata_to_uninitialized_private_account_fails_fast() -> Result<() 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()) @@ -107,8 +102,7 @@ async fn claim_pinata_to_uninitialized_private_account_fails_fast() -> Result<() let claim_result = wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Pinata(PinataProgramAgnosticSubcommand::Claim { - to: Some(winner_account_id_formatted), - to_label: None, + to: private_mention(winner_account_id), }), ) .await; @@ -139,8 +133,7 @@ async fn claim_pinata_to_existing_public_account() -> Result<()> { let pinata_prize = 150; let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim { - to: Some(format_public_account_id(ctx.existing_public_accounts()[0])), - to_label: None, + to: public_mention(ctx.existing_public_accounts()[0]), }); let pinata_balance_pre = ctx @@ -178,10 +171,7 @@ async fn claim_pinata_to_existing_private_account() -> Result<()> { let pinata_prize = 150; let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim { - to: Some(format_private_account_id( - ctx.existing_private_accounts()[0], - )), - to_label: None, + to: private_mention(ctx.existing_private_accounts()[0]), }); let pinata_balance_pre = ctx @@ -241,12 +231,9 @@ async fn claim_pinata_to_new_private_account() -> Result<()> { anyhow::bail!("Expected RegisterAccount return value"); }; - let winner_account_id_formatted = format_private_account_id(winner_account_id); - // Initialize account under auth transfer program let command = Command::AuthTransfer(AuthTransferSubcommand::Init { - account_id: Some(winner_account_id_formatted.clone()), - account_label: None, + account_id: private_mention(winner_account_id), }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -261,8 +248,7 @@ async fn claim_pinata_to_new_private_account() -> Result<()> { // Claim pinata to the new private account let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim { - to: Some(winner_account_id_formatted), - to_label: None, + to: private_mention(winner_account_id), }); let pinata_balance_pre = ctx diff --git a/integration_tests/tests/shared_accounts.rs b/integration_tests/tests/shared_accounts.rs index ecf3a4b4..9d92b519 100644 --- a/integration_tests/tests/shared_accounts.rs +++ b/integration_tests/tests/shared_accounts.rs @@ -18,14 +18,19 @@ use std::time::Duration; use anyhow::{Context as _, Result}; -use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, format_public_account_id}; +use integration_tests::{ + TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, private_mention, public_mention, +}; use log::info; use tokio::test; -use wallet::cli::{ - Command, SubcommandReturnValue, - account::{AccountSubcommand, NewSubcommand}, - group::GroupSubcommand, - programs::native_token_transfer::AuthTransferSubcommand, +use wallet::{ + account::Label, + cli::{ + Command, SubcommandReturnValue, + account::{AccountSubcommand, NewSubcommand}, + group::GroupSubcommand, + programs::native_token_transfer::AuthTransferSubcommand, + }, }; /// Create a group, create a shared account from it, and verify registration. @@ -43,8 +48,8 @@ async fn group_create_and_shared_account_registration() -> Result<()> { assert!( ctx.wallet() .storage() - .user_data - .group_key_holder("test-group") + .key_chain() + .group_key_holder(&Label::new("test-group")) .is_some() ); @@ -69,10 +74,10 @@ async fn group_create_and_shared_account_registration() -> Result<()> { let entry = ctx .wallet() .storage() - .user_data + .key_chain() .shared_private_account(&shared_account_id) .context("Shared account not found in storage")?; - assert_eq!(entry.group_label, "test-group"); + assert_eq!(entry.group_label, Label::new("test-group")); assert!(entry.pda_seed.is_none()); info!("Shared account registered: {shared_account_id}"); @@ -98,8 +103,8 @@ async fn group_invite_join_key_agreement() -> Result<()> { let sealing_sk = ctx .wallet() .storage() - .user_data - .sealing_secret_key + .key_chain() + .sealing_secret_key() .context("Sealing key not found")?; let sealing_pk = key_protocol::key_management::group_key_holder::SealingPublicKey::from_scalar(sealing_sk); @@ -107,8 +112,8 @@ async fn group_invite_join_key_agreement() -> Result<()> { let holder = ctx .wallet() .storage() - .user_data - .group_key_holder("alice-group") + .key_chain() + .group_key_holder(&Label::new("alice-group")) .context("Group not found")?; let sealed = holder.seal_for(&sealing_pk); let sealed_hex = hex::encode(&sealed); @@ -124,14 +129,14 @@ async fn group_invite_join_key_agreement() -> Result<()> { let alice_holder = ctx .wallet() .storage() - .user_data - .group_key_holder("alice-group") + .key_chain() + .group_key_holder(&Label::new("alice-group")) .unwrap(); let bob_holder = ctx .wallet() .storage() - .user_data - .group_key_holder("bob-copy") + .key_chain() + .group_key_holder(&Label::new("bob-copy")) .unwrap(); let seed = [42_u8; 32]; @@ -181,8 +186,7 @@ async fn fund_shared_account_from_public() -> Result<()> { // Initialize the shared account under auth-transfer let command = Command::AuthTransfer(AuthTransferSubcommand::Init { - account_id: Some(format!("Private/{shared_id}")), - account_label: None, + account_id: private_mention(shared_id), }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -191,10 +195,8 @@ async fn fund_shared_account_from_public() -> Result<()> { // Fund from a public account let from_public = ctx.existing_public_accounts()[0]; let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: Some(format_public_account_id(from_public)), - from_label: None, - to: Some(format!("Private/{shared_id}")), - to_label: None, + from: public_mention(from_public), + to: Some(private_mention(shared_id)), to_npk: None, to_vpk: None, to_identifier: None, @@ -212,7 +214,7 @@ async fn fund_shared_account_from_public() -> Result<()> { let entry = ctx .wallet() .storage() - .user_data + .key_chain() .shared_private_account(&shared_id) .context("Shared account not found after sync")?; diff --git a/integration_tests/tests/token.rs b/integration_tests/tests/token.rs index 6db718f9..7a7c8143 100644 --- a/integration_tests/tests/token.rs +++ b/integration_tests/tests/token.rs @@ -8,8 +8,8 @@ use std::time::Duration; use anyhow::{Context as _, 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, private_mention, public_mention, + verify_commitment_is_in_state, }; use key_protocol::key_management::key_tree::chain_index::ChainIndex; use log::info; @@ -17,10 +17,13 @@ use nssa::program::Program; use sequencer_service_rpc::RpcClient as _; use token_core::{TokenDefinition, TokenHolding}; use tokio::test; -use wallet::cli::{ - Command, SubcommandReturnValue, - account::{AccountSubcommand, NewSubcommand}, - programs::token::TokenProgramAgnosticSubcommand, +use wallet::{ + account::Label, + cli::{ + Command, SubcommandReturnValue, + account::{AccountSubcommand, NewSubcommand}, + programs::token::TokenProgramAgnosticSubcommand, + }, }; #[test] @@ -79,10 +82,8 @@ async fn create_and_transfer_public_token() -> Result<()> { let name = "A NAME".to_owned(); let total_supply = 37; let subcommand = TokenProgramAgnosticSubcommand::New { - definition_account_id: Some(format_public_account_id(definition_account_id)), - definition_account_label: None, - supply_account_id: Some(format_public_account_id(supply_account_id)), - supply_account_label: None, + definition_account_id: public_mention(definition_account_id), + supply_account_id: public_mention(supply_account_id), name: name.clone(), total_supply, }; @@ -128,10 +129,8 @@ async fn create_and_transfer_public_token() -> Result<()> { // Transfer 7 tokens from supply_acc to recipient_account_id let transfer_amount = 7; let subcommand = TokenProgramAgnosticSubcommand::Send { - from: Some(format_public_account_id(supply_account_id)), - from_label: None, - to: Some(format_public_account_id(recipient_account_id)), - to_label: None, + from: public_mention(supply_account_id), + to: Some(public_mention(recipient_account_id)), to_npk: None, to_vpk: None, to_identifier: Some(0), @@ -176,10 +175,8 @@ async fn create_and_transfer_public_token() -> Result<()> { // Burn 3 tokens from recipient_acc let burn_amount = 3; let subcommand = TokenProgramAgnosticSubcommand::Burn { - definition: Some(format_public_account_id(definition_account_id)), - definition_label: None, - holder: Some(format_public_account_id(recipient_account_id)), - holder_label: None, + definition: public_mention(definition_account_id), + holder: public_mention(recipient_account_id), amount: burn_amount, }; @@ -222,10 +219,8 @@ async fn create_and_transfer_public_token() -> Result<()> { // Mint 10 tokens at recipient_acc let mint_amount = 10; let subcommand = TokenProgramAgnosticSubcommand::Mint { - definition: Some(format_public_account_id(definition_account_id)), - definition_label: None, - holder: Some(format_public_account_id(recipient_account_id)), - holder_label: None, + definition: public_mention(definition_account_id), + holder: Some(public_mention(recipient_account_id)), holder_npk: None, holder_vpk: None, holder_identifier: None, @@ -329,10 +324,8 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> { let name = "A NAME".to_owned(); let total_supply = 37; let subcommand = TokenProgramAgnosticSubcommand::New { - definition_account_id: Some(format_public_account_id(definition_account_id)), - definition_account_label: None, - supply_account_id: Some(format_private_account_id(supply_account_id)), - supply_account_label: None, + definition_account_id: public_mention(definition_account_id), + supply_account_id: private_mention(supply_account_id), name: name.clone(), total_supply, }; @@ -368,10 +361,8 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> { // Transfer 7 tokens from supply_acc to recipient_account_id let transfer_amount = 7; let subcommand = TokenProgramAgnosticSubcommand::Send { - from: Some(format_private_account_id(supply_account_id)), - from_label: None, - to: Some(format_private_account_id(recipient_account_id)), - to_label: None, + from: private_mention(supply_account_id), + to: Some(private_mention(recipient_account_id)), to_npk: None, to_vpk: None, to_identifier: Some(0), @@ -398,10 +389,8 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> { // Burn 3 tokens from recipient_acc let burn_amount = 3; let subcommand = TokenProgramAgnosticSubcommand::Burn { - definition: Some(format_public_account_id(definition_account_id)), - definition_label: None, - holder: Some(format_private_account_id(recipient_account_id)), - holder_label: None, + definition: public_mention(definition_account_id), + holder: private_mention(recipient_account_id), amount: burn_amount, }; @@ -492,10 +481,8 @@ async fn create_token_with_private_definition() -> Result<()> { let name = "A NAME".to_owned(); let total_supply = 37; let subcommand = TokenProgramAgnosticSubcommand::New { - definition_account_id: Some(format_private_account_id(definition_account_id)), - definition_account_label: None, - supply_account_id: Some(format_public_account_id(supply_account_id)), - supply_account_label: None, + definition_account_id: private_mention(definition_account_id), + supply_account_id: public_mention(supply_account_id), name: name.clone(), total_supply, }; @@ -563,10 +550,8 @@ async fn create_token_with_private_definition() -> Result<()> { // Mint to public account let mint_amount_public = 10; let subcommand = TokenProgramAgnosticSubcommand::Mint { - definition: Some(format_private_account_id(definition_account_id)), - definition_label: None, - holder: Some(format_public_account_id(recipient_account_id_public)), - holder_label: None, + definition: private_mention(definition_account_id), + holder: Some(public_mention(recipient_account_id_public)), holder_npk: None, holder_vpk: None, holder_identifier: None, @@ -612,10 +597,8 @@ async fn create_token_with_private_definition() -> Result<()> { // Mint to private account let mint_amount_private = 5; let subcommand = TokenProgramAgnosticSubcommand::Mint { - definition: Some(format_private_account_id(definition_account_id)), - definition_label: None, - holder: Some(format_private_account_id(recipient_account_id_private)), - holder_label: None, + definition: private_mention(definition_account_id), + holder: Some(private_mention(recipient_account_id_private)), holder_npk: None, holder_vpk: None, holder_identifier: None, @@ -694,10 +677,8 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> { let name = "A NAME".to_owned(); let total_supply = 37; let subcommand = TokenProgramAgnosticSubcommand::New { - definition_account_id: Some(format_private_account_id(definition_account_id)), - definition_account_label: None, - supply_account_id: Some(format_private_account_id(supply_account_id)), - supply_account_label: None, + definition_account_id: private_mention(definition_account_id), + supply_account_id: private_mention(supply_account_id), name, total_supply, }; @@ -755,10 +736,8 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> { // Transfer tokens let transfer_amount = 7; let subcommand = TokenProgramAgnosticSubcommand::Send { - from: Some(format_private_account_id(supply_account_id)), - from_label: None, - to: Some(format_private_account_id(recipient_account_id)), - to_label: None, + from: private_mention(supply_account_id), + to: Some(private_mention(recipient_account_id)), to_npk: None, to_vpk: None, to_identifier: Some(0), @@ -871,10 +850,8 @@ async fn shielded_token_transfer() -> Result<()> { let name = "A NAME".to_owned(); let total_supply = 37; let subcommand = TokenProgramAgnosticSubcommand::New { - definition_account_id: Some(format_public_account_id(definition_account_id)), - definition_account_label: None, - supply_account_id: Some(format_public_account_id(supply_account_id)), - supply_account_label: None, + definition_account_id: public_mention(definition_account_id), + supply_account_id: public_mention(supply_account_id), name, total_supply, }; @@ -887,10 +864,8 @@ async fn shielded_token_transfer() -> Result<()> { // Perform shielded transfer: public supply -> private recipient let transfer_amount = 7; let subcommand = TokenProgramAgnosticSubcommand::Send { - from: Some(format_public_account_id(supply_account_id)), - from_label: None, - to: Some(format_private_account_id(recipient_account_id)), - to_label: None, + from: public_mention(supply_account_id), + to: Some(private_mention(recipient_account_id)), to_npk: None, to_vpk: None, to_identifier: Some(0), @@ -998,10 +973,8 @@ async fn deshielded_token_transfer() -> Result<()> { let name = "A NAME".to_owned(); let total_supply = 37; let subcommand = TokenProgramAgnosticSubcommand::New { - definition_account_id: Some(format_public_account_id(definition_account_id)), - definition_account_label: None, - supply_account_id: Some(format_private_account_id(supply_account_id)), - supply_account_label: None, + definition_account_id: public_mention(definition_account_id), + supply_account_id: private_mention(supply_account_id), name, total_supply, }; @@ -1014,10 +987,8 @@ async fn deshielded_token_transfer() -> Result<()> { // Perform deshielded transfer: private supply -> public recipient let transfer_amount = 7; let subcommand = TokenProgramAgnosticSubcommand::Send { - from: Some(format_private_account_id(supply_account_id)), - from_label: None, - to: Some(format_public_account_id(recipient_account_id)), - to_label: None, + from: private_mention(supply_account_id), + to: Some(public_mention(recipient_account_id)), to_npk: None, to_vpk: None, to_identifier: Some(0), @@ -1109,10 +1080,8 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> { let name = "A NAME".to_owned(); let total_supply = 37; let subcommand = TokenProgramAgnosticSubcommand::New { - definition_account_id: Some(format_private_account_id(definition_account_id)), - definition_account_label: None, - supply_account_id: Some(format_private_account_id(supply_account_id)), - supply_account_label: None, + definition_account_id: private_mention(definition_account_id), + supply_account_id: private_mention(supply_account_id), name, total_supply, }; @@ -1139,22 +1108,23 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> { }; // Get keys for foreign mint (claiming path) - let (holder_keys, _, holder_identifier) = ctx + let holder = ctx .wallet() .storage() - .user_data - .get_private_account(recipient_account_id) + .key_chain() + .private_account(recipient_account_id) .context("Failed to get private account keys")?; + let holder_keys = holder.key_chain; + let holder_identifier = holder.identifier; + // Mint using claiming path (foreign account) let mint_amount = 9; let subcommand = TokenProgramAgnosticSubcommand::Mint { - definition: Some(format_private_account_id(definition_account_id)), - definition_label: None, + definition: private_mention(definition_account_id), holder: None, - 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_vpk: Some(hex::encode(&holder_keys.viewing_public_key.0)), holder_identifier: Some(holder_identifier), amount: mint_amount, }; @@ -1199,8 +1169,8 @@ async fn create_token_using_labels() -> Result<()> { let mut ctx = TestContext::new().await?; // Create definition and supply accounts with labels - let def_label = "token-definition-label".to_owned(); - let supply_label = "token-supply-label".to_owned(); + let def_label = Label::new("token-definition-label"); + let supply_label = Label::new("token-supply-label"); let result = wallet::cli::execute_subcommand( ctx.wallet_mut(), @@ -1221,7 +1191,7 @@ async fn create_token_using_labels() -> Result<()> { ctx.wallet_mut(), Command::Account(AccountSubcommand::New(NewSubcommand::Public { cci: None, - label: Some(supply_label.clone()), + label: Some(Label::new(supply_label.clone())), })), ) .await?; @@ -1236,10 +1206,8 @@ async fn create_token_using_labels() -> Result<()> { let name = "LABELED TOKEN".to_owned(); let total_supply = 100; let subcommand = TokenProgramAgnosticSubcommand::New { - definition_account_id: None, - definition_account_label: Some(def_label), - supply_account_id: None, - supply_account_label: Some(supply_label), + definition_account_id: def_label.into(), + supply_account_id: supply_label.into(), name: name.clone(), total_supply, }; @@ -1303,7 +1271,7 @@ async fn transfer_token_using_from_label() -> Result<()> { }; // Create supply account with a label - let supply_label = "token-supply-sender".to_owned(); + let supply_label = Label::new("token-supply-sender"); let result = wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Account(AccountSubcommand::New(NewSubcommand::Public { @@ -1338,10 +1306,8 @@ async fn transfer_token_using_from_label() -> Result<()> { // Create token let total_supply = 50; let subcommand = TokenProgramAgnosticSubcommand::New { - definition_account_id: Some(format_public_account_id(definition_account_id)), - definition_account_label: None, - supply_account_id: Some(format_public_account_id(supply_account_id)), - supply_account_label: None, + definition_account_id: public_mention(definition_account_id), + supply_account_id: public_mention(supply_account_id), name: "LABEL TEST TOKEN".to_owned(), total_supply, }; @@ -1353,10 +1319,8 @@ async fn transfer_token_using_from_label() -> Result<()> { // Transfer token using from_label instead of from let transfer_amount = 20; let subcommand = TokenProgramAgnosticSubcommand::Send { - from: None, - from_label: Some(supply_label), - to: Some(format_public_account_id(recipient_account_id)), - to_label: None, + from: supply_label.into(), + to: Some(public_mention(recipient_account_id)), to_npk: None, to_vpk: None, to_identifier: Some(0), diff --git a/integration_tests/tests/tps.rs b/integration_tests/tests/tps.rs index df74daba..dc8e235d 100644 --- a/integration_tests/tests/tps.rs +++ b/integration_tests/tests/tps.rs @@ -14,11 +14,8 @@ use std::time::{Duration, Instant}; use anyhow::Result; use bytesize::ByteSize; use common::transaction::NSSATransaction; -use integration_tests::{ - TestContext, - config::{InitialData, SequencerPartialConfig}, -}; -use key_protocol::key_management::{KeyChain, ephemeral_key_holder::EphemeralKeyHolder}; +use integration_tests::{TestContext, config::SequencerPartialConfig}; +use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder; use log::info; use nssa::{ Account, AccountId, PrivacyPreservingTransaction, PrivateKey, PublicKey, PublicTransaction, @@ -31,6 +28,7 @@ use nssa_core::{ account::{AccountWithMetadata, Nonce, data::Data}, encryption::ViewingPublicKey, }; +use sequencer_core::config::GenesisTransaction; use sequencer_service_rpc::RpcClient as _; use tokio::test; @@ -81,7 +79,7 @@ impl TpsTestManager { program.id(), [pair[0].1, pair[1].1].to_vec(), [Nonce(0_u128)].to_vec(), - amount, + authenticated_transfer_core::Instruction::Transfer { amount }, ) .unwrap(); let witness_set = @@ -96,28 +94,14 @@ impl TpsTestManager { /// Generates a sequencer configuration with initial balance in a number of public accounts. /// The transactions generated with the function `build_public_txs` will be valid in a node /// started with the config from this method. - fn generate_initial_data(&self) -> InitialData { - // Create public public keypairs - let public_accounts = self - .public_keypairs + fn generate_genesis(&self) -> Vec { + self.public_keypairs .iter() - .map(|(key, _)| (key.clone(), 10)) - .collect(); - - // Generate an initial commitment to be used with the privacy preserving transaction - // created with the `build_privacy_transaction` function. - let key_chain = KeyChain::new_os_random(); - let account = Account { - balance: 100, - nonce: Nonce(0xdead_beef), - program_owner: Program::authenticated_transfer_program().id(), - data: Data::default(), - }; - - InitialData { - public_accounts, - private_accounts: vec![(key_chain, account)], - } + .map(|(_, account_id)| GenesisTransaction::SupplyPublicAccount { + account_id: *account_id, + balance: 10, + }) + .collect() } const fn generate_sequencer_partial_config() -> SequencerPartialConfig { @@ -139,7 +123,7 @@ pub async fn tps_test() -> Result<()> { let tps_test = TpsTestManager::new(target_tps, num_transactions); let ctx = TestContext::builder() .with_sequencer_partial_config(TpsTestManager::generate_sequencer_partial_config()) - .with_initial_data(tps_test.generate_initial_data()) + .with_genesis(tps_test.generate_genesis()) .build() .await?; @@ -166,7 +150,7 @@ pub async fn tps_test() -> Result<()> { loop { assert!( now.elapsed().as_millis() <= target_time.as_millis(), - "TPS test failed by timeout" + "TPS test failed by timeout, transactions processed {i}/{num_transactions}" ); let tx_obj = ctx @@ -250,7 +234,10 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { ); let (output, proof) = circuit::execute_and_prove( vec![sender_pre, recipient_pre], - Program::serialize_instruction(balance_to_move).unwrap(), + Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer { + amount: balance_to_move, + }) + .unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { ssk: sender_ss, diff --git a/integration_tests/tests/wallet_ffi.rs b/integration_tests/tests/wallet_ffi.rs index db84b066..11524eb7 100644 --- a/integration_tests/tests/wallet_ffi.rs +++ b/integration_tests/tests/wallet_ffi.rs @@ -24,6 +24,7 @@ use log::info; use nssa::{Account, AccountId, PrivateKey, PublicKey, program::Program}; use nssa_core::program::DEFAULT_PROGRAM_ID; use tempfile::tempdir; +use wallet::account::HumanReadableAccount; use wallet_ffi::{ FfiAccount, FfiAccountList, FfiBytes32, FfiPrivateAccountKeys, FfiPublicAccountKey, FfiTransferResult, FfiU128, WalletHandle, error, @@ -53,11 +54,24 @@ unsafe extern "C" { out_account_id: *mut FfiBytes32, ) -> error::WalletFfiError; + fn wallet_ffi_import_public_account( + handle: *mut WalletHandle, + private_key_hex: *const c_char, + ) -> error::WalletFfiError; + fn wallet_ffi_create_private_accounts_key( handle: *mut WalletHandle, out_keys: *mut FfiPrivateAccountKeys, ) -> error::WalletFfiError; + fn wallet_ffi_import_private_account( + handle: *mut WalletHandle, + key_chain_json: *const c_char, + chain_index: *const c_char, + identifier: *const FfiU128, + account_state_json: *const c_char, + ) -> error::WalletFfiError; + fn wallet_ffi_list_accounts( handle: *mut WalletHandle, out_list: *mut FfiAccountList, @@ -191,13 +205,59 @@ fn new_wallet_ffi_with_test_context_config( let storage_path = CString::new(storage_path.to_str().unwrap())?; let password = CString::new(ctx.ctx().wallet_password())?; - Ok(unsafe { + let wallet_ffi_handle = unsafe { wallet_ffi_create_new( config_path.as_ptr(), storage_path.as_ptr(), password.as_ptr(), ) - }) + }; + + // Import accounts from source wallet + let source_wallet = ctx.ctx().wallet(); + let source_key_chain = source_wallet.storage().key_chain(); + + for (account_id, _chain_index) in source_key_chain.public_account_ids() { + let private_key_hex = source_wallet + .get_account_public_signing_key(account_id) + .unwrap() + .to_string(); + let private_key_hex = CString::new(private_key_hex)?; + unsafe { wallet_ffi_import_public_account(wallet_ffi_handle, private_key_hex.as_ptr()) } + .unwrap(); + } + + for (account_id, _chain_index) in source_key_chain.private_account_ids() { + let account = source_key_chain.private_account(account_id).unwrap(); + let key_chain_json = CString::new(serde_json::to_string(account.key_chain)?)?; + let account_state_json = CString::new(serde_json::to_string( + &HumanReadableAccount::from(account.account.clone()), + )?)?; + + let chain_index = account + .chain_index + .map(|chain_index| CString::new(chain_index.to_string())) + .transpose()?; + let chain_index_ptr = chain_index + .as_ref() + .map_or(std::ptr::null(), |value| value.as_ptr()); + let identifier = FfiU128 { + data: account.identifier.to_le_bytes(), + }; + + unsafe { + wallet_ffi_import_private_account( + wallet_ffi_handle, + key_chain_json.as_ptr(), + chain_index_ptr, + &raw const identifier, + account_state_json.as_ptr(), + ) + } + .unwrap(); + } + + Ok(wallet_ffi_handle) } fn new_wallet_ffi_with_default_config(password: &str) -> Result<*mut WalletHandle> { @@ -405,7 +465,7 @@ fn test_wallet_ffi_get_balance_public() -> Result<()> { let balance = unsafe { let mut out_balance: [u8; 16] = [0; 16]; - let ffi_account_id = FfiBytes32::from(&account_id); + let ffi_account_id = FfiBytes32::from(account_id); wallet_ffi_get_balance( wallet_ffi_handle, &raw const ffi_account_id, @@ -435,7 +495,7 @@ fn test_wallet_ffi_get_account_public() -> Result<()> { let mut out_account = FfiAccount::default(); let account: Account = unsafe { - let ffi_account_id = FfiBytes32::from(&account_id); + let ffi_account_id = FfiBytes32::from(account_id); wallet_ffi_get_account_public( wallet_ffi_handle, &raw const ffi_account_id, @@ -451,7 +511,7 @@ fn test_wallet_ffi_get_account_public() -> Result<()> { ); assert_eq!(account.balance, 10000); assert!(account.data.is_empty()); - assert_eq!(account.nonce.0, 0); + assert_eq!(account.nonce.0, 1); unsafe { wallet_ffi_free_account_data(&raw mut out_account); @@ -472,7 +532,7 @@ fn test_wallet_ffi_get_account_private() -> Result<()> { let mut out_account = FfiAccount::default(); let account: Account = unsafe { - let ffi_account_id = FfiBytes32::from(&account_id); + let ffi_account_id = FfiBytes32::from(account_id); wallet_ffi_get_account_private( wallet_ffi_handle, &raw const ffi_account_id, @@ -488,7 +548,6 @@ fn test_wallet_ffi_get_account_private() -> Result<()> { ); assert_eq!(account.balance, 10000); assert!(account.data.is_empty()); - assert_eq!(account.nonce, 0_u128.into()); unsafe { wallet_ffi_free_account_data(&raw mut out_account); @@ -509,7 +568,7 @@ fn test_wallet_ffi_get_public_account_keys() -> Result<()> { let mut out_key = FfiPublicAccountKey::default(); let key: PublicKey = unsafe { - let ffi_account_id = FfiBytes32::from(&account_id); + let ffi_account_id = FfiBytes32::from(account_id); wallet_ffi_get_public_account_key( wallet_ffi_handle, &raw const ffi_account_id, @@ -548,7 +607,7 @@ fn test_wallet_ffi_get_private_account_keys() -> Result<()> { let mut keys = FfiPrivateAccountKeys::default(); unsafe { - let ffi_account_id = FfiBytes32::from(&account_id); + let ffi_account_id = FfiBytes32::from(account_id); wallet_ffi_get_private_account_keys( wallet_ffi_handle, &raw const ffi_account_id, @@ -557,15 +616,15 @@ fn test_wallet_ffi_get_private_account_keys() -> Result<()> { .unwrap(); }; - let key_chain = &ctx + let account = &ctx .ctx() .wallet() .storage() - .user_data - .get_private_account(account_id) - .unwrap() - .0; + .key_chain() + .private_account(account_id) + .unwrap(); + let key_chain = account.key_chain; let expected_npk = &key_chain.nullifier_public_key; let expected_vpk = &key_chain.viewing_public_key; @@ -587,7 +646,7 @@ fn test_wallet_ffi_account_id_to_base58() -> Result<()> { let private_key = PrivateKey::new_os_random(); let public_key = PublicKey::new_from_private_key(&private_key); let account_id = AccountId::from(&public_key); - let ffi_bytes: FfiBytes32 = (&account_id).into(); + let ffi_bytes: FfiBytes32 = account_id.into(); let ptr = unsafe { wallet_ffi_account_id_to_base58(&raw const ffi_bytes) }; let ffi_result = unsafe { CStr::from_ptr(ptr).to_str()? }; @@ -744,8 +803,8 @@ fn test_wallet_ffi_transfer_public() -> Result<()> { let ctx = BlockingTestContext::new()?; let home = tempfile::tempdir()?; 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: FfiBytes32 = (&ctx.ctx().existing_public_accounts()[1]).into(); + let from: FfiBytes32 = ctx.ctx().existing_public_accounts()[0].into(); + let to: FfiBytes32 = ctx.ctx().existing_public_accounts()[1].into(); let amount: [u8; 16] = 100_u128.to_le_bytes(); let mut transfer_result = FfiTransferResult::default(); @@ -797,12 +856,12 @@ fn test_wallet_ffi_transfer_shielded() -> Result<()> { let ctx = BlockingTestContext::new()?; let home = tempfile::tempdir()?; 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 from: FfiBytes32 = ctx.ctx().existing_public_accounts()[0].into(); let (to, to_keys) = unsafe { let mut out_keys = FfiPrivateAccountKeys::default(); 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(); + let to: FfiBytes32 = account_id.into(); (to, out_keys) }; let amount: [u8; 16] = 100_u128.to_le_bytes(); @@ -871,8 +930,8 @@ fn test_wallet_ffi_transfer_deshielded() -> Result<()> { let ctx = BlockingTestContext::new()?; let home = tempfile::tempdir()?; let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; - let from: FfiBytes32 = (&ctx.ctx().existing_private_accounts()[0]).into(); - let to: FfiBytes32 = (&ctx.ctx().existing_public_accounts()[0]).into(); + let from: FfiBytes32 = ctx.ctx().existing_private_accounts()[0].into(); + let to: FfiBytes32 = ctx.ctx().existing_public_accounts()[0].into(); let amount: [u8; 16] = 100_u128.to_le_bytes(); let mut transfer_result = FfiTransferResult::default(); @@ -883,8 +942,9 @@ fn test_wallet_ffi_transfer_deshielded() -> Result<()> { &raw const to, &raw const amount, &raw mut transfer_result, - ); + ) } + .unwrap(); info!("Waiting for next block creation"); std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)); @@ -892,9 +952,9 @@ fn test_wallet_ffi_transfer_deshielded() -> Result<()> { // Sync private account local storage with onchain encrypted state unsafe { let mut current_height = 0; - wallet_ffi_get_current_block_height(wallet_ffi_handle, &raw mut current_height); - wallet_ffi_sync_to_block(wallet_ffi_handle, current_height); - }; + wallet_ffi_get_current_block_height(wallet_ffi_handle, &raw mut current_height).unwrap(); + wallet_ffi_sync_to_block(wallet_ffi_handle, current_height).unwrap(); + } let from_balance = unsafe { let mut out_balance: [u8; 16] = [0; 16]; @@ -931,12 +991,12 @@ fn test_wallet_ffi_transfer_private() -> Result<()> { let home = tempfile::tempdir()?; let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; - let from: FfiBytes32 = (&ctx.ctx().existing_private_accounts()[0]).into(); + let from: FfiBytes32 = ctx.ctx().existing_private_accounts()[0].into(); let (to, to_keys) = unsafe { let mut out_keys = FfiPrivateAccountKeys::default(); 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(); + let to: FfiBytes32 = account_id.into(); (to, out_keys) }; diff --git a/key_protocol/Cargo.toml b/key_protocol/Cargo.toml index 72829ca8..a0b5c397 100644 --- a/key_protocol/Cargo.toml +++ b/key_protocol/Cargo.toml @@ -7,6 +7,10 @@ license = { workspace = true } [lints] workspace = true +[features] +default = [] +test_utils = [] + [dependencies] nssa.workspace = true nssa_core.workspace = true diff --git a/key_protocol/src/key_management/ephemeral_key_holder.rs b/key_protocol/src/key_management/ephemeral_key_holder.rs index 6ef9e305..7a6dc7d0 100644 --- a/key_protocol/src/key_management/ephemeral_key_holder.rs +++ b/key_protocol/src/key_management/ephemeral_key_holder.rs @@ -36,7 +36,7 @@ impl EphemeralKeyHolder { &self, receiver_viewing_public_key: &ViewingPublicKey, ) -> SharedSecretKey { - SharedSecretKey::new(&self.ephemeral_secret_key, receiver_viewing_public_key) + SharedSecretKey::new(self.ephemeral_secret_key, receiver_viewing_public_key) } } @@ -47,7 +47,7 @@ pub fn produce_one_sided_shared_secret_receiver( let mut esk = [0; 32]; OsRng.fill_bytes(&mut esk); ( - SharedSecretKey::new(&esk, vpk), + SharedSecretKey::new(esk, vpk), EphemeralPublicKey::from_scalar(esk), ) } diff --git a/key_protocol/src/key_management/group_key_holder.rs b/key_protocol/src/key_management/group_key_holder.rs index 609c45ed..faa1b10a 100644 --- a/key_protocol/src/key_management/group_key_holder.rs +++ b/key_protocol/src/key_management/group_key_holder.rs @@ -59,7 +59,7 @@ pub type SealingSecretKey = Scalar; /// `Debug` is implemented manually to redact the GMS; formatting this value with `{:?}` /// will not leak the secret. Code that formats through `{:#?}` on containing types is /// safe for the same reason. -#[derive(Serialize, Deserialize, Clone)] +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct GroupKeyHolder { gms: [u8; 32], } @@ -164,7 +164,7 @@ impl GroupKeyHolder { let mut ephemeral_scalar: Scalar = [0_u8; 32]; OsRng.fill_bytes(&mut ephemeral_scalar); let ephemeral_pubkey = Secp256k1Point::from_scalar(ephemeral_scalar); - let shared = SharedSecretKey::new(&ephemeral_scalar, &recipient_key.0); + let shared = SharedSecretKey::new(ephemeral_scalar, &recipient_key.0); let aes_key = Self::seal_kdf(&shared); let cipher = Aes256Gcm::new(&aes_key.into()); @@ -191,7 +191,7 @@ impl GroupKeyHolder { /// /// Returns `Err` if the ciphertext is too short, the ECDH point is invalid, or the /// AES-GCM authentication tag doesn't verify (wrong key or tampered data). - pub fn unseal(sealed: &[u8], own_key: &SealingSecretKey) -> Result { + pub fn unseal(sealed: &[u8], own_key: SealingSecretKey) -> Result { const HEADER_LEN: usize = 33 + 12; const MIN_LEN: usize = HEADER_LEN + 16; if sealed.len() < MIN_LEN { @@ -407,7 +407,7 @@ mod tests { let recipient_vsk = recipient_keys.viewing_secret_key; let sealed = holder.seal_for(&SealingPublicKey::from_bytes(recipient_vpk.0)); - let restored = GroupKeyHolder::unseal(&sealed, &recipient_vsk).expect("unseal"); + let restored = GroupKeyHolder::unseal(&sealed, recipient_vsk).expect("unseal"); assert_eq!(restored.dangerous_raw_gms(), holder.dangerous_raw_gms()); @@ -438,7 +438,7 @@ mod tests { .viewing_secret_key; let sealed = holder.seal_for(&SealingPublicKey::from_bytes(recipient_vpk.0)); - let result = GroupKeyHolder::unseal(&sealed, &wrong_vsk); + let result = GroupKeyHolder::unseal(&sealed, wrong_vsk); assert!(matches!(result, Err(super::SealError::DecryptionFailed))); } @@ -457,7 +457,7 @@ mod tests { let last = sealed.len() - 1; sealed[last] ^= 0xFF; - let result = GroupKeyHolder::unseal(&sealed, &recipient_vsk); + let result = GroupKeyHolder::unseal(&sealed, recipient_vsk); assert!(matches!(result, Err(super::SealError::DecryptionFailed))); } @@ -481,7 +481,7 @@ mod tests { #[test] fn unseal_too_short_fails() { let vsk: SealingSecretKey = [7_u8; 32]; - let result = GroupKeyHolder::unseal(&[0_u8; 10], &vsk); + let result = GroupKeyHolder::unseal(&[0_u8; 10], vsk); assert!(matches!(result, Err(super::SealError::TooShort))); } @@ -537,7 +537,7 @@ mod tests { let sealed = alice_holder.seal_for(&SealingPublicKey::from_bytes(bob_vpk.0)); let bob_holder = - GroupKeyHolder::unseal(&sealed, &bob_vsk).expect("Bob should unseal the GMS"); + GroupKeyHolder::unseal(&sealed, bob_vsk).expect("Bob should unseal the GMS"); // Key agreement: both derive identical NPK and AccountId let bob_npk = bob_holder 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 6ffc8119..94a4ba7b 100644 --- a/key_protocol/src/key_management/key_tree/keys_private.rs +++ b/key_protocol/src/key_management/key_tree/keys_private.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use k256::{Scalar, elliptic_curve::PrimeField as _}; use nssa_core::{Identifier, NullifierPublicKey, encryption::ViewingPublicKey}; use serde::{Deserialize, Serialize}; @@ -9,8 +11,9 @@ use crate::key_management::{ }; #[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(any(test, feature = "test_utils"), derive(PartialEq, Eq))] pub struct ChildKeysPrivate { - pub value: (KeyChain, Vec<(Identifier, nssa::Account)>), + pub value: (KeyChain, BTreeMap), pub ccc: [u8; 32], /// Can be [`None`] if root. pub cci: Option, @@ -47,7 +50,7 @@ impl ChildKeysPrivate { viewing_secret_key: vsk, }, }, - vec![], + BTreeMap::from_iter([(0, nssa::Account::default())]), ), ccc, cci: None, @@ -97,7 +100,7 @@ impl ChildKeysPrivate { viewing_secret_key: vsk, }, }, - vec![], + BTreeMap::from_iter([(0, nssa::Account::default())]), ), ccc, cci: Some(cci), @@ -115,7 +118,7 @@ impl KeyTreeNode for ChildKeysPrivate { } fn account_ids(&self) -> impl Iterator { - self.value.1.iter().map(|(identifier, _)| { + self.value.1.keys().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 3ab9cc35..4671795d 100644 --- a/key_protocol/src/key_management/key_tree/keys_public.rs +++ b/key_protocol/src/key_management/key_tree/keys_public.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; use crate::key_management::key_tree::traits::KeyTreeNode; #[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(any(test, feature = "test_utils"), derive(PartialEq, Eq))] pub struct ChildKeysPublic { pub csk: nssa::PrivateKey, pub cpk: nssa::PublicKey, diff --git a/key_protocol/src/key_management/key_tree/mod.rs b/key_protocol/src/key_management/key_tree/mod.rs index 0ae0a52f..4f2605f1 100644 --- a/key_protocol/src/key_management/key_tree/mod.rs +++ b/key_protocol/src/key_management/key_tree/mod.rs @@ -21,6 +21,7 @@ pub mod traits; pub const DEPTH_SOFT_CAP: u32 = 20; #[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(any(test, feature = "test_utils"), derive(PartialEq, Eq))] pub struct KeyTree { pub key_map: BTreeMap, pub account_id_map: BTreeMap, @@ -297,7 +298,13 @@ impl KeyTree { 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() { + if node.value.1.is_empty() + || node + .value + .1 + .iter() + .all(|(_, acc)| acc == &nssa::Account::default()) + { let account_ids = node.account_ids(); self.key_map.remove(&id); for addr in account_ids { @@ -531,49 +538,49 @@ mod tests { .key_map .get_mut(&ChainIndex::from_str("/1").unwrap()) .unwrap(); - acc.value.1.push(( + acc.value.1.insert( 0, nssa::Account { balance: 2, ..nssa::Account::default() }, - )); + ); let acc = tree .key_map .get_mut(&ChainIndex::from_str("/2").unwrap()) .unwrap(); - acc.value.1.push(( + acc.value.1.insert( 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.push(( + acc.value.1.insert( 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.push(( + acc.value.1.insert( 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"] { @@ -605,15 +612,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[0].1.balance, 2); + assert_eq!(acc.value.1[&0].balance, 2); let acc = &tree.key_map[&ChainIndex::from_str("/2").unwrap()]; - assert_eq!(acc.value.1[0].1.balance, 3); + assert_eq!(acc.value.1[&0].balance, 3); let acc = &tree.key_map[&ChainIndex::from_str("/0/1").unwrap()]; - assert_eq!(acc.value.1[0].1.balance, 5); + assert_eq!(acc.value.1[&0].balance, 5); let acc = &tree.key_map[&ChainIndex::from_str("/1/0").unwrap()]; - assert_eq!(acc.value.1[0].1.balance, 6); + assert_eq!(acc.value.1[&0].balance, 6); } } diff --git a/key_protocol/src/key_management/mod.rs b/key_protocol/src/key_management/mod.rs index aa5a1a75..ad98d7e2 100644 --- a/key_protocol/src/key_management/mod.rs +++ b/key_protocol/src/key_management/mod.rs @@ -12,8 +12,8 @@ pub mod secret_holders; pub type PublicAccountSigningKey = [u8; 32]; -#[derive(Serialize, Deserialize, Clone, Debug)] -/// Entrypoint to key management. +/// Private account keychain. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct KeyChain { pub secret_spending_key: SecretSpendingKey, pub private_key_holder: PrivateKeyHolder, @@ -72,7 +72,7 @@ impl KeyChain { index: Option, ) -> SharedSecretKey { SharedSecretKey::new( - &self.secret_spending_key.generate_viewing_secret_key(index), + self.secret_spending_key.generate_viewing_secret_key(index), ephemeral_public_key_sender, ) } diff --git a/key_protocol/src/key_management/secret_holders.rs b/key_protocol/src/key_management/secret_holders.rs index 9804ba39..f5e71ca8 100644 --- a/key_protocol/src/key_management/secret_holders.rs +++ b/key_protocol/src/key_management/secret_holders.rs @@ -17,14 +17,14 @@ pub struct SeedHolder { } /// Secret spending key object. Can produce `PrivateKeyHolder` objects. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct SecretSpendingKey(pub [u8; 32]); pub type ViewingSecretKey = Scalar; -#[derive(Serialize, Deserialize, Debug, Clone)] /// Private key holder. Produces public keys. Can produce `account_id`. Can produce shared secret /// for recepient. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct PrivateKeyHolder { pub nullifier_secret_key: NullifierSecretKey, pub viewing_secret_key: ViewingSecretKey, diff --git a/key_protocol/src/key_protocol_core/mod.rs b/key_protocol/src/key_protocol_core/mod.rs deleted file mode 100644 index 20bea342..00000000 --- a/key_protocol/src/key_protocol_core/mod.rs +++ /dev/null @@ -1,417 +0,0 @@ -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::{ - KeyChain, - group_key_holder::GroupKeyHolder, - key_tree::{KeyTreePrivate, KeyTreePublic, chain_index::ChainIndex}, - secret_holders::SeedHolder, -}; - -pub type PublicKey = AffinePoint; - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct UserPrivateAccountData { - pub key_chain: KeyChain, - pub accounts: Vec<(Identifier, Account)>, -} - -/// Metadata for a shared account (GMS-derived), stored alongside the cached plaintext state. -/// The group label and identifier (or PDA seed) are needed to re-derive keys during sync. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct SharedAccountEntry { - pub group_label: String, - pub identifier: Identifier, - /// For PDA accounts, the seed and program ID used to derive keys via `derive_keys_for_pda`. - /// `None` for regular shared accounts (keys derived from identifier via derivation seed). - #[serde(default)] - pub pda_seed: Option, - #[serde(default)] - pub pda_program_id: Option, - pub account: Account, -} - -#[derive(Clone, Debug)] -pub struct NSSAUserData { - /// Default public accounts. - pub default_pub_account_signing_keys: BTreeMap, - /// Default private accounts. - pub default_user_private_accounts: BTreeMap, - /// Tree of public keys. - pub public_key_tree: KeyTreePublic, - /// Tree of private keys. - pub private_key_tree: KeyTreePrivate, - /// Group key holders for shared account management, keyed by a human-readable label. - pub group_key_holders: BTreeMap, - /// Cached plaintext state of shared private accounts (PDAs and regular shared accounts), - /// keyed by `AccountId`. Each entry stores the group label and identifier needed - /// to re-derive keys during sync. - pub shared_private_accounts: BTreeMap, - /// Dedicated sealing secret key for GMS distribution. Generated once via - /// `wallet group new-sealing-key`. The corresponding public key is shared with - /// group members so they can seal GMS for this wallet. - pub sealing_secret_key: Option, -} - -impl NSSAUserData { - fn valid_public_key_transaction_pairing_check( - 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(&nssa::PublicKey::new_from_private_key(key)); - if &expected_account_id != account_id { - println!("{expected_account_id}, {account_id}"); - check_res = false; - } - } - check_res - } - - fn valid_private_key_transaction_pairing_check( - accounts_keys_map: &BTreeMap, - ) -> bool { - let mut check_res = true; - 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; - } - } - check_res - } - - pub fn new_with_accounts( - default_accounts_keys: BTreeMap, - default_accounts_key_chains: BTreeMap, - public_key_tree: KeyTreePublic, - private_key_tree: KeyTreePrivate, - ) -> Result { - if !Self::valid_public_key_transaction_pairing_check(&default_accounts_keys) { - anyhow::bail!( - "Key transaction pairing check not satisfied, there are public account_ids, which are not derived from keys" - ); - } - - if !Self::valid_private_key_transaction_pairing_check(&default_accounts_key_chains) { - anyhow::bail!( - "Key transaction pairing check not satisfied, there are private account_ids, which are not derived from keys" - ); - } - - Ok(Self { - default_pub_account_signing_keys: default_accounts_keys, - default_user_private_accounts: default_accounts_key_chains, - public_key_tree, - private_key_tree, - group_key_holders: BTreeMap::new(), - shared_private_accounts: BTreeMap::new(), - sealing_secret_key: None, - }) - } - - /// Generated new private key for public transaction signatures. - /// - /// Returns the `account_id` of new account. - pub fn generate_new_public_transaction_private_key( - &mut self, - parent_cci: Option, - ) -> (nssa::AccountId, ChainIndex) { - match parent_cci { - Some(parent_cci) => self - .public_key_tree - .generate_new_public_node(&parent_cci) - .expect("Parent must be present in a tree"), - None => self - .public_key_tree - .generate_new_public_node_layered() - .expect("Search for new node slot failed"), - } - } - - /// Returns the signing key for public transaction signatures. - #[must_use] - pub fn get_pub_account_signing_key( - &self, - account_id: nssa::AccountId, - ) -> Option<&nssa::PrivateKey> { - self.default_pub_account_signing_keys - .get(&account_id) - .or_else(|| self.public_key_tree.get_node(account_id).map(Into::into)) - } - - /// 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 - .create_private_accounts_key_node(&parent_cci) - .expect("Parent must be present in a tree"), - None => self - .private_key_tree - .create_private_accounts_key_node_layered() - .expect("Search for new node slot failed"), - } - } - - /// 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, 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 { - self.public_account_ids().chain(self.private_account_ids()) - } - - pub fn public_account_ids(&self) -> impl Iterator { - self.default_pub_account_signing_keys - .keys() - .copied() - .chain(self.public_key_tree.account_id_map.keys().copied()) - } - - pub fn private_account_ids(&self) -> impl Iterator { - self.default_user_private_accounts - .keys() - .copied() - .chain(self.private_key_tree.account_id_map.keys().copied()) - } - - /// Returns the `GroupKeyHolder` for the given label, if it exists. - #[must_use] - pub fn group_key_holder(&self, label: &str) -> Option<&GroupKeyHolder> { - self.group_key_holders.get(label) - } - - /// Inserts or replaces a `GroupKeyHolder` under the given label. - /// - /// If a holder already exists under this label, it is silently replaced and the old - /// GMS is lost. Callers must ensure label uniqueness across groups. - pub fn insert_group_key_holder(&mut self, label: String, holder: GroupKeyHolder) { - self.group_key_holders.insert(label, holder); - } - - /// Returns the cached account for a shared private account, if it exists. - #[must_use] - pub fn shared_private_account( - &self, - account_id: &nssa::AccountId, - ) -> Option<&SharedAccountEntry> { - self.shared_private_accounts.get(account_id) - } - - /// Inserts or replaces a shared private account entry. - pub fn insert_shared_private_account( - &mut self, - account_id: nssa::AccountId, - entry: SharedAccountEntry, - ) { - self.shared_private_accounts.insert(account_id, entry); - } - - /// Updates the cached account state for a shared private account. - pub fn update_shared_private_account_state( - &mut self, - account_id: &nssa::AccountId, - account: nssa_core::account::Account, - ) { - if let Some(entry) = self.shared_private_accounts.get_mut(account_id) { - entry.account = account; - } - } - - /// Iterates over all shared private accounts. - pub fn shared_private_accounts_iter( - &self, - ) -> impl Iterator { - self.shared_private_accounts.iter() - } -} - -impl Default for NSSAUserData { - fn default() -> Self { - let (seed_holder, _mnemonic) = SeedHolder::new_mnemonic(""); - Self::new_with_accounts( - BTreeMap::new(), - BTreeMap::new(), - KeyTreePublic::new(&seed_holder), - KeyTreePrivate::new(&seed_holder), - ) - .unwrap() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn group_key_holder_storage_round_trip() { - let mut user_data = NSSAUserData::default(); - assert!(user_data.group_key_holder("test-group").is_none()); - - let holder = GroupKeyHolder::from_gms([42_u8; 32]); - user_data.insert_group_key_holder(String::from("test-group"), holder.clone()); - - let retrieved = user_data - .group_key_holder("test-group") - .expect("should exist"); - assert_eq!(retrieved.dangerous_raw_gms(), holder.dangerous_raw_gms()); - } - - #[test] - fn group_key_holders_default_empty() { - let user_data = NSSAUserData::default(); - assert!(user_data.group_key_holders.is_empty()); - assert!(user_data.shared_private_accounts.is_empty()); - } - - #[test] - fn shared_account_entry_serde_round_trip() { - use nssa_core::program::PdaSeed; - - let entry = SharedAccountEntry { - group_label: String::from("test-group"), - identifier: 42, - pda_seed: None, - pda_program_id: None, - account: nssa_core::account::Account::default(), - }; - let encoded = bincode::serialize(&entry).expect("serialize"); - let decoded: SharedAccountEntry = bincode::deserialize(&encoded).expect("deserialize"); - assert_eq!(decoded.group_label, "test-group"); - assert_eq!(decoded.identifier, 42); - assert!(decoded.pda_seed.is_none()); - - let pda_entry = SharedAccountEntry { - group_label: String::from("pda-group"), - identifier: u128::MAX, - pda_seed: Some(PdaSeed::new([7_u8; 32])), - pda_program_id: Some([9; 8]), - account: nssa_core::account::Account::default(), - }; - let pda_encoded = bincode::serialize(&pda_entry).expect("serialize pda"); - let pda_decoded: SharedAccountEntry = - bincode::deserialize(&pda_encoded).expect("deserialize pda"); - assert_eq!(pda_decoded.group_label, "pda-group"); - assert_eq!(pda_decoded.identifier, u128::MAX); - assert_eq!(pda_decoded.pda_seed.unwrap(), PdaSeed::new([7_u8; 32])); - } - - #[test] - fn shared_account_entry_none_pda_seed_round_trips() { - // Verify that an entry with pda_seed=None serializes and deserializes correctly, - // confirming the #[serde(default)] attribute works for backward compatibility. - let entry = SharedAccountEntry { - group_label: String::from("old"), - identifier: 1, - pda_seed: None, - pda_program_id: None, - account: nssa_core::account::Account::default(), - }; - let encoded = bincode::serialize(&entry).expect("serialize"); - let decoded: SharedAccountEntry = bincode::deserialize(&encoded).expect("deserialize"); - assert_eq!(decoded.group_label, "old"); - assert_eq!(decoded.identifier, 1); - assert!(decoded.pda_seed.is_none()); - } - - #[test] - fn shared_account_derives_consistent_keys_from_group() { - use nssa_core::program::PdaSeed; - - let mut user_data = NSSAUserData::default(); - let gms_holder = GroupKeyHolder::from_gms([42_u8; 32]); - user_data.insert_group_key_holder(String::from("my-group"), gms_holder); - - let holder = user_data.group_key_holder("my-group").unwrap(); - - // Regular shared account: derive via tag - let tag = [1_u8; 32]; - let keys_a = holder.derive_keys_for_shared_account(&tag); - let keys_b = holder.derive_keys_for_shared_account(&tag); - assert_eq!( - keys_a.generate_nullifier_public_key(), - keys_b.generate_nullifier_public_key(), - ); - - // PDA shared account: derive via seed - let seed = PdaSeed::new([2_u8; 32]); - let pda_keys_a = holder.derive_keys_for_pda(&[9; 8], &seed); - let pda_keys_b = holder.derive_keys_for_pda(&[9; 8], &seed); - assert_eq!( - pda_keys_a.generate_nullifier_public_key(), - pda_keys_b.generate_nullifier_public_key(), - ); - - // PDA and shared derivations don't collide - assert_ne!( - keys_a.generate_nullifier_public_key(), - pda_keys_a.generate_nullifier_public_key(), - ); - } - - #[test] - fn new_account() { - let mut user_data = NSSAUserData::default(); - - 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 key_chain = &user_data.private_key_tree.key_map[&chain_index].value.0; - println!("{key_chain:#?}"); - } -} diff --git a/key_protocol/src/lib.rs b/key_protocol/src/lib.rs index e3fe31cf..a8c333e4 100644 --- a/key_protocol/src/lib.rs +++ b/key_protocol/src/lib.rs @@ -1,4 +1,3 @@ #![expect(clippy::print_stdout, reason = "TODO: fix later")] pub mod key_management; -pub mod key_protocol_core; diff --git a/nssa/Cargo.toml b/nssa/Cargo.toml index d8f0807c..d2141999 100644 --- a/nssa/Cargo.toml +++ b/nssa/Cargo.toml @@ -30,6 +30,7 @@ risc0-binfmt = "3.0.2" [dev-dependencies] token_core.workspace = true +authenticated_transfer_core.workspace = true test_program_methods.workspace = true env_logger.workspace = true diff --git a/nssa/core/src/encryption/shared_key_derivation.rs b/nssa/core/src/encryption/shared_key_derivation.rs index 8169e8f9..8ea5aac8 100644 --- a/nssa/core/src/encryption/shared_key_derivation.rs +++ b/nssa/core/src/encryption/shared_key_derivation.rs @@ -17,7 +17,9 @@ use serde::{Deserialize, Serialize}; use crate::{SharedSecretKey, encryption::Scalar}; -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +#[derive( + Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, BorshSerialize, BorshDeserialize, +)] pub struct Secp256k1Point(pub Vec); impl std::fmt::Debug for Secp256k1Point { @@ -56,8 +58,8 @@ impl From<&EphemeralSecretKey> for EphemeralPublicKey { impl SharedSecretKey { /// Creates a new shared secret key from a scalar and a point. #[must_use] - pub fn new(scalar: &Scalar, point: &Secp256k1Point) -> Self { - let scalar = k256::Scalar::from_repr((*scalar).into()).unwrap(); + pub fn new(scalar: Scalar, point: &Secp256k1Point) -> Self { + let scalar = k256::Scalar::from_repr(scalar.into()).unwrap(); let point: [u8; 33] = point.0.clone().try_into().unwrap(); let encoded = EncodedPoint::from_bytes(point).unwrap(); diff --git a/nssa/core/src/lib.rs b/nssa/core/src/lib.rs index d660aed0..0ddf7b8a 100644 --- a/nssa/core/src/lib.rs +++ b/nssa/core/src/lib.rs @@ -24,6 +24,8 @@ pub mod program; #[cfg(feature = "host")] pub mod error; +pub const GENESIS_BLOCK_ID: BlockId = 1; + pub type BlockId = u64; /// Unix timestamp in milliseconds. pub type Timestamp = u64; diff --git a/nssa/core/src/nullifier.rs b/nssa/core/src/nullifier.rs index aafe3f7c..6de0998e 100644 --- a/nssa/core/src/nullifier.rs +++ b/nssa/core/src/nullifier.rs @@ -8,7 +8,7 @@ const PRIVATE_ACCOUNT_ID_PREFIX: &[u8; 32] = b"/LEE/v0.3/AccountId/Private/\x00\ pub type Identifier = u128; -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] #[cfg_attr(any(feature = "host", test), derive(Hash))] pub struct NullifierPublicKey(pub [u8; 32]); diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index e4e33932..cc4f82de 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -636,7 +636,6 @@ pub fn validate_execution( } // 8. Total balance is preserved - let Some(total_balance_pre_states) = WrappedBalanceSum::from_balances(pre_states.iter().map(|pre| pre.account.balance)) else { diff --git a/nssa/src/lib.rs b/nssa/src/lib.rs index f4c3be9d..0ad9b143 100644 --- a/nssa/src/lib.rs +++ b/nssa/src/lib.rs @@ -4,7 +4,7 @@ )] pub use nssa_core::{ - SharedSecretKey, + GENESIS_BLOCK_ID, SharedSecretKey, account::{Account, AccountId, Data}, encryption::EphemeralPublicKey, program::ProgramId, diff --git a/nssa/src/merkle_tree/mod.rs b/nssa/src/merkle_tree/mod.rs index 588f0f60..e439d092 100644 --- a/nssa/src/merkle_tree/mod.rs +++ b/nssa/src/merkle_tree/mod.rs @@ -17,6 +17,26 @@ pub struct MerkleTree { } impl MerkleTree { + pub fn with_capacity(capacity: usize) -> Self { + // Adjust capacity to ensure power of two + let capacity = capacity.next_power_of_two(); + let total_depth = usize::try_from(capacity.trailing_zeros()).expect("u32 fits in usize"); + + let nodes = default_values::DEFAULT_VALUES[..=total_depth] + .iter() + .rev() + .enumerate() + .flat_map(|(level, default_value)| std::iter::repeat_n(default_value, 1 << level)) + .copied() + .collect(); + + Self { + nodes, + capacity, + length: 0, + } + } + pub fn root(&self) -> Node { let root_index = self.root_index(); *self.get_node(root_index) @@ -49,26 +69,6 @@ impl MerkleTree { self.nodes[index] = node; } - pub fn with_capacity(capacity: usize) -> Self { - // Adjust capacity to ensure power of two - let capacity = capacity.next_power_of_two(); - let total_depth = usize::try_from(capacity.trailing_zeros()).expect("u32 fits in usize"); - - let nodes = default_values::DEFAULT_VALUES[..=total_depth] - .iter() - .rev() - .enumerate() - .flat_map(|(level, default_value)| std::iter::repeat_n(default_value, 1 << level)) - .copied() - .collect(); - - Self { - nodes, - capacity, - length: 0, - } - } - /// Reallocates storage of Merkle tree for double capacity. /// The current tree is embedded into the new tree as a subtree. fn reallocate_to_double_capacity(&mut self) { diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index ff23647e..8343a450 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -228,11 +228,14 @@ mod tests { let expected_sender_pre = sender.clone(); let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(&esk, &recipient_keys.vpk()); + let shared_secret = SharedSecretKey::new(esk, &recipient_keys.vpk()); let (output, proof) = execute_and_prove( vec![sender, recipient], - Program::serialize_instruction(balance_to_move).unwrap(), + Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer { + amount: balance_to_move, + }) + .unwrap(), vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivateUnauthorized { @@ -322,14 +325,17 @@ mod tests { ]; let esk_1 = [3; 32]; - let shared_secret_1 = SharedSecretKey::new(&esk_1, &sender_keys.vpk()); + let shared_secret_1 = SharedSecretKey::new(esk_1, &sender_keys.vpk()); let esk_2 = [5; 32]; - let shared_secret_2 = SharedSecretKey::new(&esk_2, &recipient_keys.vpk()); + let shared_secret_2 = SharedSecretKey::new(esk_2, &recipient_keys.vpk()); let (output, proof) = execute_and_prove( vec![sender_pre, recipient], - Program::serialize_instruction(balance_to_move).unwrap(), + Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer { + amount: balance_to_move, + }) + .unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { ssk: shared_secret_1, @@ -397,7 +403,7 @@ mod tests { .unwrap(); let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(&esk, &account_keys.vpk()); + let shared_secret = SharedSecretKey::new(esk, &account_keys.vpk()); let program_with_deps = ProgramWithDependencies::new( validity_window_chain_caller, @@ -428,7 +434,7 @@ mod tests { let keys = test_private_account_keys_1(); let npk = keys.npk(); let seed = PdaSeed::new([42; 32]); - let shared_secret_pda = SharedSecretKey::new(&[55; 32], &keys.vpk()); + let shared_secret_pda = SharedSecretKey::new([55; 32], &keys.vpk()); // PDA (new, private PDA) — AccountId derived from auth_transfer_proxy's program ID let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk); @@ -465,7 +471,7 @@ mod tests { let keys = test_private_account_keys_1(); let npk = keys.npk(); let seed = PdaSeed::new([42; 32]); - let shared_secret_pda = SharedSecretKey::new(&[55; 32], &keys.vpk()); + let shared_secret_pda = SharedSecretKey::new([55; 32], &keys.vpk()); // PDA (new, private PDA) let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk); @@ -518,7 +524,7 @@ mod tests { let shared_keys = test_private_account_keys_1(); let shared_npk = shared_keys.npk(); let shared_identifier: u128 = 42; - let shared_secret = SharedSecretKey::new(&[55; 32], &shared_keys.vpk()); + let shared_secret = SharedSecretKey::new([55; 32], &shared_keys.vpk()); // Sender: public account with balance, owned by auth-transfer let sender_id = AccountId::new([99; 32]); diff --git a/nssa/src/privacy_preserving_transaction/message.rs b/nssa/src/privacy_preserving_transaction/message.rs index 697f66ac..39b2dc80 100644 --- a/nssa/src/privacy_preserving_transaction/message.rs +++ b/nssa/src/privacy_preserving_transaction/message.rs @@ -250,7 +250,7 @@ pub mod tests { 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 shared_secret = SharedSecretKey::new(esk, &vpk); let epk = EphemeralPublicKey::from_scalar(esk); let ciphertext = EncryptionScheme::encrypt(&account, 0, &shared_secret, &commitment, 2); let encrypted_account_data = diff --git a/nssa/src/public_transaction/execution.rs b/nssa/src/public_transaction/execution.rs new file mode 100644 index 00000000..a5dc1cd9 --- /dev/null +++ b/nssa/src/public_transaction/execution.rs @@ -0,0 +1,118 @@ +use std::collections::{HashMap, VecDeque}; + +use log::debug; +use nssa_core::{ + account::{Account, AccountId, AccountWithMetadata}, + program::{ChainedCall, ProgramId, ProgramOutput}, +}; + +use crate::{PublicTransaction, V03State, error::NssaError}; + +pub trait Validator { + fn validate_pre_execution(&mut self) -> Result<(), NssaError>; + + fn on_chained_call(&mut self) -> Result<(), NssaError>; + + fn validate_output( + &mut self, + state_diff: &HashMap, + caller_program_id: Option, + chained_call: &ChainedCall, + program_output: &ProgramOutput, + ) -> Result<(), NssaError>; + + fn validate_post_execution( + &mut self, + state_diff: &HashMap, + ) -> Result<(), NssaError>; +} + +pub fn execute( + mut validator: impl Validator, + tx: &PublicTransaction, + state: &V03State, +) -> Result, NssaError> { + validator.validate_pre_execution()?; + + let message = tx.message(); + let signer_account_ids = tx.signer_account_ids(); + + // Build pre_states for execution + let input_pre_states: Vec<_> = message + .account_ids + .iter() + .map(|account_id| { + AccountWithMetadata::new( + state.get_account_by_id(*account_id), + signer_account_ids.contains(account_id), + *account_id, + ) + }) + .collect(); + + let mut state_diff: HashMap = HashMap::new(); + + let initial_call = ChainedCall { + program_id: message.program_id, + instruction_data: message.instruction_data.clone(), + pre_states: input_pre_states, + pda_seeds: vec![], + }; + + let mut chained_calls = VecDeque::from_iter([(initial_call, None)]); + + while let Some((chained_call, caller_program_id)) = chained_calls.pop_front() { + validator.on_chained_call()?; + + // Check that the `program_id` corresponds to a deployed program + let Some(program) = state.programs().get(&chained_call.program_id) else { + return Err(NssaError::InvalidInput("Unknown program".into())); + }; + + debug!( + "Program {:?} pre_states: {:?}, instruction_data: {:?}", + chained_call.program_id, chained_call.pre_states, chained_call.instruction_data + ); + let mut program_output = program.execute( + caller_program_id, + &chained_call.pre_states, + &chained_call.instruction_data, + )?; + debug!( + "Program {:?} output: {:?}", + chained_call.program_id, program_output + ); + + validator.validate_output( + &state_diff, + caller_program_id, + &chained_call, + &program_output, + )?; + + for post in program_output + .post_states + .iter_mut() + .filter(|post| post.required_claim().is_some()) + { + post.account_mut().program_owner = chained_call.program_id; + } + + // Update the state diff + for (pre, post) in program_output + .pre_states + .iter() + .zip(program_output.post_states.iter()) + { + state_diff.insert(pre.account_id, post.account().clone()); + } + + for new_call in program_output.chained_calls.into_iter().rev() { + chained_calls.push_front((new_call, Some(chained_call.program_id))); + } + } + + validator.validate_post_execution(&state_diff)?; + + Ok(state_diff) +} diff --git a/nssa/src/public_transaction/mod.rs b/nssa/src/public_transaction/mod.rs index 1af61e10..108dbf6e 100644 --- a/nssa/src/public_transaction/mod.rs +++ b/nssa/src/public_transaction/mod.rs @@ -1,7 +1,9 @@ +pub use execution::{Validator, execute}; pub use message::Message; pub use transaction::PublicTransaction; pub use witness_set::WitnessSet; +mod execution; mod message; mod transaction; mod witness_set; diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 9e4d8524..a281b5aa 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -122,7 +122,22 @@ pub struct V03State { programs: HashMap, } +impl Default for V03State { + fn default() -> Self { + Self { + public_state: HashMap::new(), + private_state: (CommitmentSet::with_capacity(32), NullifierSet::new()), + programs: HashMap::new(), + } + } +} + impl V03State { + #[must_use] + pub fn new() -> Self { + Self::default() + } + #[must_use] pub fn new_with_genesis_accounts( initial_data: &[(AccountId, u128)], @@ -361,6 +376,7 @@ pub mod tests { use std::collections::HashMap; + use authenticated_transfer_core::Instruction as AuthTransferInstruction; use nssa_core::{ BlockId, Commitment, InputAccountIdentity, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, Timestamp, @@ -525,8 +541,13 @@ pub mod tests { let account_ids = vec![from, to]; let nonces = vec![Nonce(from_nonce), Nonce(to_nonce)]; let program_id = Program::authenticated_transfer_program().id(); - let message = - public_transaction::Message::try_new(program_id, account_ids, nonces, balance).unwrap(); + let message = public_transaction::Message::try_new( + program_id, + account_ids, + nonces, + AuthTransferInstruction::Transfer { amount: balance }, + ) + .unwrap(); let witness_set = public_transaction::WitnessSet::for_message(&message, &[from_key, to_key]); PublicTransaction::new(message, witness_set) @@ -1206,7 +1227,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1, 0); + let result = state.transition_from_public_transaction(&tx, 2, 0); assert!(matches!( result, @@ -1240,7 +1261,7 @@ pub mod tests { .unwrap(); let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1, 0); + let result = state.transition_from_public_transaction(&tx, 2, 0); assert!(matches!( result, @@ -1288,12 +1309,15 @@ pub mod tests { AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(&esk, &recipient_keys.vpk()); + let shared_secret = SharedSecretKey::new(esk, &recipient_keys.vpk()); let epk = EphemeralPublicKey::from_scalar(esk); let (output, proof) = circuit::execute_and_prove( vec![sender, recipient], - Program::serialize_instruction(balance_to_move).unwrap(), + Program::serialize_instruction(AuthTransferInstruction::Transfer { + amount: balance_to_move, + }) + .unwrap(), vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivateUnauthorized { @@ -1337,16 +1361,19 @@ pub mod tests { 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()); + let shared_secret_1 = SharedSecretKey::new(esk_1, &sender_keys.vpk()); let epk_1 = EphemeralPublicKey::from_scalar(esk_1); let esk_2 = [3; 32]; - let shared_secret_2 = SharedSecretKey::new(&esk_2, &recipient_keys.vpk()); + let shared_secret_2 = SharedSecretKey::new(esk_2, &recipient_keys.vpk()); let epk_2 = EphemeralPublicKey::from_scalar(esk_2); let (output, proof) = circuit::execute_and_prove( vec![sender_pre, recipient_pre], - Program::serialize_instruction(balance_to_move).unwrap(), + Program::serialize_instruction(AuthTransferInstruction::Transfer { + amount: balance_to_move, + }) + .unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { ssk: shared_secret_1, @@ -1404,12 +1431,15 @@ pub mod tests { ); let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(&esk, &sender_keys.vpk()); + let shared_secret = SharedSecretKey::new(esk, &sender_keys.vpk()); let epk = EphemeralPublicKey::from_scalar(esk); let (output, proof) = circuit::execute_and_prove( vec![sender_pre, recipient_pre], - Program::serialize_instruction(balance_to_move).unwrap(), + Program::serialize_instruction(AuthTransferInstruction::Transfer { + amount: balance_to_move, + }) + .unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { ssk: shared_secret, @@ -1910,14 +1940,14 @@ pub mod tests { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), + ssk: SharedSecretKey::new([55; 32], &sender_keys.vpk()), nsk: recipient_keys.nsk, membership_proof: (0, vec![]), identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { npk: recipient_keys.npk(), - ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + ssk: SharedSecretKey::new([56; 32], &recipient_keys.vpk()), identifier: 0, }, ], @@ -1956,14 +1986,14 @@ pub mod tests { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), + ssk: SharedSecretKey::new([55; 32], &sender_keys.vpk()), nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { npk: recipient_keys.npk(), - ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + ssk: SharedSecretKey::new([56; 32], &recipient_keys.vpk()), identifier: 0, }, ], @@ -2002,14 +2032,14 @@ pub mod tests { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), + ssk: SharedSecretKey::new([55; 32], &sender_keys.vpk()), nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { npk: recipient_keys.npk(), - ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + ssk: SharedSecretKey::new([56; 32], &recipient_keys.vpk()), identifier: 0, }, ], @@ -2048,14 +2078,14 @@ pub mod tests { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), + ssk: SharedSecretKey::new([55; 32], &sender_keys.vpk()), nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { npk: recipient_keys.npk(), - ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + ssk: SharedSecretKey::new([56; 32], &recipient_keys.vpk()), identifier: 0, }, ], @@ -2094,14 +2124,14 @@ pub mod tests { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), + ssk: SharedSecretKey::new([55; 32], &sender_keys.vpk()), nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { npk: recipient_keys.npk(), - ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + ssk: SharedSecretKey::new([56; 32], &recipient_keys.vpk()), identifier: 0, }, ], @@ -2138,14 +2168,14 @@ pub mod tests { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), + ssk: SharedSecretKey::new([55; 32], &sender_keys.vpk()), nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { npk: recipient_keys.npk(), - ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + ssk: SharedSecretKey::new([56; 32], &recipient_keys.vpk()), identifier: 0, }, ], @@ -2164,7 +2194,7 @@ pub mod tests { let program = Program::simple_balance_transfer(); let keys = test_private_account_keys_1(); let npk = keys.npk(); - let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk()); + let shared_secret = SharedSecretKey::new([55; 32], &keys.vpk()); let public_account_1 = AccountWithMetadata::new( Account { program_owner: program.id(), @@ -2204,7 +2234,7 @@ pub mod tests { let keys = test_private_account_keys_1(); let npk = keys.npk(); let seed = PdaSeed::new([42; 32]); - let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk()); + let shared_secret = SharedSecretKey::new([55; 32], &keys.vpk()); let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk); let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); @@ -2239,7 +2269,7 @@ pub mod tests { let npk_a = keys_a.npk(); let npk_b = keys_b.npk(); let seed = PdaSeed::new([42; 32]); - let shared_secret = SharedSecretKey::new(&[55; 32], &keys_b.vpk()); + let shared_secret = SharedSecretKey::new([55; 32], &keys_b.vpk()); // `account_id` is derived from `npk_a`, but `npk_b` is supplied for this pre_state. // `AccountId::for_private_pda(program, seed, npk_b) != account_id`, so the claim check in @@ -2272,7 +2302,7 @@ pub mod tests { let keys = test_private_account_keys_1(); let npk = keys.npk(); let seed = PdaSeed::new([77; 32]); - let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk()); + let shared_secret = SharedSecretKey::new([55; 32], &keys.vpk()); let account_id = AccountId::for_private_pda(&delegator.id(), &seed, &npk); let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); @@ -2309,7 +2339,7 @@ pub mod tests { let npk = keys.npk(); let claim_seed = PdaSeed::new([77; 32]); let wrong_delegated_seed = PdaSeed::new([88; 32]); - let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk()); + let shared_secret = SharedSecretKey::new([55; 32], &keys.vpk()); let account_id = AccountId::for_private_pda(&delegator.id(), &claim_seed, &npk); let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); @@ -2345,8 +2375,8 @@ pub mod tests { let keys_a = test_private_account_keys_1(); let keys_b = test_private_account_keys_2(); let seed = PdaSeed::new([55; 32]); - let shared_a = SharedSecretKey::new(&[66; 32], &keys_a.vpk()); - let shared_b = SharedSecretKey::new(&[77; 32], &keys_b.vpk()); + let shared_a = SharedSecretKey::new([66; 32], &keys_a.vpk()); + let shared_b = SharedSecretKey::new([77; 32], &keys_b.vpk()); let account_a = AccountId::for_private_pda(&program.id(), &seed, &keys_a.npk()); let account_b = AccountId::for_private_pda(&program.id(), &seed, &keys_b.npk()); @@ -2389,7 +2419,7 @@ pub mod tests { let program = Program::noop(); let keys = test_private_account_keys_1(); let npk = keys.npk(); - let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk()); + let shared_secret = SharedSecretKey::new([55; 32], &keys.vpk()); let seed = PdaSeed::new([99; 32]); // Simulate a previously-claimed private PDA: program_owner != DEFAULT, is_authorized = @@ -2487,7 +2517,7 @@ pub mod tests { (&sender_keys.npk(), 0), ); - let shared_secret = SharedSecretKey::new(&[55; 32], &sender_keys.vpk()); + let shared_secret = SharedSecretKey::new([55; 32], &sender_keys.vpk()); let result = execute_and_prove( vec![private_account_1.clone(), private_account_1], Program::serialize_instruction(100_u128).unwrap(), @@ -2538,7 +2568,7 @@ pub mod tests { program.id(), vec![from, to], vec![Nonce(0), Nonce(0)], - amount, + AuthTransferInstruction::Transfer { amount }, ) .unwrap(); let witness_set = @@ -2561,15 +2591,19 @@ pub mod tests { assert_eq!(state.get_account_by_id(account_id), Account::default()); - let message = - public_transaction::Message::try_new(program.id(), vec![account_id], vec![], 0_u128) - .unwrap(); + let message = public_transaction::Message::try_new( + program.id(), + vec![account_id], + vec![], + AuthTransferInstruction::Initialize, + ) + .unwrap(); let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1, 0); + let result = state.transition_from_public_transaction(&tx, 2, 0); - assert!(matches!(result, Err(NssaError::ProgramExecutionFailed(_)))); + assert!(matches!(result, Err(NssaError::InvalidProgramBehavior(_)))); assert_eq!(state.get_account_by_id(account_id), Account::default()); } @@ -2586,7 +2620,7 @@ pub mod tests { program.id(), vec![account_id], vec![Nonce(0)], - 0_u128, + AuthTransferInstruction::Initialize, ) .unwrap(); let witness_set = public_transaction::WitnessSet::for_message(&message, &[&account_key]); @@ -2794,12 +2828,12 @@ pub mod tests { let result = execute_and_prove( vec![public_account], - Program::serialize_instruction(0_u128).unwrap(), + Program::serialize_instruction(AuthTransferInstruction::Initialize).unwrap(), vec![InputAccountIdentity::Public], &program.into(), ); - assert!(matches!(result, Err(NssaError::ProgramProveFailed(_)))); + assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); } #[test] @@ -2828,12 +2862,17 @@ pub mod tests { let recipient_pre = AccountWithMetadata::new(Account::default(), true, recipient_account_id); let esk = [5; 32]; - let shared_secret = SharedSecretKey::new(&esk, &sender_keys.vpk()); + let shared_secret = SharedSecretKey::new(esk, &sender_keys.vpk()); let epk = EphemeralPublicKey::from_scalar(esk); + let balance = 37; + let (output, proof) = execute_and_prove( vec![sender_pre, recipient_pre], - Program::serialize_instruction(37_u128).unwrap(), + Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer { + amount: balance, + }) + .unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { ssk: shared_secret, @@ -2871,7 +2910,7 @@ pub mod tests { state.get_account_by_id(recipient_account_id), Account { program_owner: program_id, - balance: 37, + balance, nonce: Nonce(1), ..Account::default() } @@ -2929,11 +2968,11 @@ pub mod tests { ); let from_esk = [3; 32]; - let from_ss = SharedSecretKey::new(&from_esk, &from_keys.vpk()); + let from_ss = SharedSecretKey::new(from_esk, &from_keys.vpk()); let from_epk = EphemeralPublicKey::from_scalar(from_esk); let to_esk = [3; 32]; - let to_ss = SharedSecretKey::new(&to_esk, &to_keys.vpk()); + let to_ss = SharedSecretKey::new(to_esk, &to_keys.vpk()); let to_epk = EphemeralPublicKey::from_scalar(to_esk); let mut dependencies = HashMap::new(); @@ -3139,7 +3178,7 @@ pub mod tests { /// This test ensures that even if a malicious program tries to perform overflow of balances /// it will not be able to break the balance validation. #[test] - fn malicious_program_cannot_break_balance_validation() { + fn malicious_program_cannot_break_balance_validation_if_not_in_genesis() { let sender_key = PrivateKey::try_new([37; 32]).unwrap(); let sender_id = AccountId::from(&PublicKey::new_from_private_key(&sender_key)); let sender_init_balance: u128 = 10; @@ -3178,7 +3217,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[&sender_key]); let tx = PublicTransaction::new(message, witness_set); - let res = state.transition_from_public_transaction(&tx, 1, 0); + let res = state.transition_from_public_transaction(&tx, 2, 0); let expected_total_balance_pre_states = WrappedBalanceSum::from_balances( [sender_init_balance, recipient_init_balance].into_iter(), ) @@ -3232,16 +3271,15 @@ pub mod tests { // Set up parameters for the new account let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(&esk, &private_keys.vpk()); + let shared_secret = SharedSecretKey::new(esk, &private_keys.vpk()); let epk = EphemeralPublicKey::from_scalar(esk); - // Balance to initialize the account with (0 for a new account) - let balance: u128 = 0; + let instruction = authenticated_transfer_core::Instruction::Initialize; // Execute and prove the circuit with the authorized account but no commitment proof let (output, proof) = execute_and_prove( vec![authorized_account], - Program::serialize_instruction(balance).unwrap(), + Program::serialize_instruction(instruction).unwrap(), vec![InputAccountIdentity::PrivateAuthorizedInit { ssk: shared_secret, nsk: private_keys.nsk, @@ -3285,7 +3323,7 @@ pub mod tests { let program = Program::claimer(); let esk = [5; 32]; - let shared_secret = SharedSecretKey::new(&esk, &private_keys.vpk()); + let shared_secret = SharedSecretKey::new(esk, &private_keys.vpk()); let epk = EphemeralPublicKey::from_scalar(esk); let (output, proof) = execute_and_prove( @@ -3335,15 +3373,15 @@ pub mod tests { // Set up parameters for claiming the new account let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(&esk, &private_keys.vpk()); + let shared_secret = SharedSecretKey::new(esk, &private_keys.vpk()); let epk = EphemeralPublicKey::from_scalar(esk); - let balance: u128 = 0; + let instruction = authenticated_transfer_core::Instruction::Initialize; // Step 2: Execute claimer program to claim the account with authentication let (output, proof) = execute_and_prove( vec![authorized_account.clone()], - Program::serialize_instruction(balance).unwrap(), + Program::serialize_instruction(instruction).unwrap(), vec![InputAccountIdentity::PrivateAuthorizedInit { ssk: shared_secret, nsk: private_keys.nsk, @@ -3385,7 +3423,7 @@ pub mod tests { let noop_program = Program::noop(); let esk2 = [4; 32]; - let shared_secret2 = SharedSecretKey::new(&esk2, &private_keys.vpk()); + let shared_secret2 = SharedSecretKey::new(esk2, &private_keys.vpk()); // Step 3: Try to execute noop program with authentication but without initialization let res = execute_and_prove( @@ -3469,7 +3507,7 @@ pub mod tests { vec![private_account], Program::serialize_instruction(instruction).unwrap(), vec![InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new(&[3; 32], &sender_keys.vpk()), + ssk: SharedSecretKey::new([3; 32], &sender_keys.vpk()), nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, @@ -3495,7 +3533,7 @@ pub mod tests { vec![private_account], Program::serialize_instruction(instruction).unwrap(), vec![InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new(&[3; 32], &sender_keys.vpk()), + ssk: SharedSecretKey::new([3; 32], &sender_keys.vpk()), nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, @@ -3542,7 +3580,7 @@ pub mod tests { let instruction = (balance_to_transfer, auth_transfers.id()); let recipient_esk = [3; 32]; - let recipient = SharedSecretKey::new(&recipient_esk, &recipient_keys.vpk()); + let recipient = SharedSecretKey::new(recipient_esk, &recipient_keys.vpk()); let mut dependencies = HashMap::new(); dependencies.insert(auth_transfers.id(), auth_transfers); @@ -3699,7 +3737,7 @@ pub mod tests { let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); let tx = { let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(&esk, &account_keys.vpk()); + let shared_secret = SharedSecretKey::new(esk, &account_keys.vpk()); let epk = EphemeralPublicKey::from_scalar(esk); let instruction = ( @@ -3769,7 +3807,7 @@ pub mod tests { let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); let tx = { let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(&esk, &account_keys.vpk()); + let shared_secret = SharedSecretKey::new(esk, &account_keys.vpk()); let epk = EphemeralPublicKey::from_scalar(esk); let instruction = ( diff --git a/nssa/src/validated_state_diff.rs b/nssa/src/validated_state_diff.rs index 455a13a6..f7044b51 100644 --- a/nssa/src/validated_state_diff.rs +++ b/nssa/src/validated_state_diff.rs @@ -1,14 +1,14 @@ use std::{ - collections::{HashMap, HashSet, VecDeque}, + collections::{HashMap, HashSet}, hash::Hash, }; -use log::debug; use nssa_core::{ BlockId, Commitment, Nullifier, PrivacyPreservingCircuitOutput, Timestamp, account::{Account, AccountId, AccountWithMetadata}, program::{ - ChainedCall, Claim, DEFAULT_PROGRAM_ID, compute_public_authorized_pdas, validate_execution, + ChainedCall, Claim, DEFAULT_PROGRAM_ID, ProgramId, ProgramOutput, + compute_public_authorized_pdas, validate_execution, }, }; @@ -45,237 +45,11 @@ impl ValidatedStateDiff { block_id: BlockId, timestamp: Timestamp, ) -> Result { - let message = tx.message(); - let witness_set = tx.witness_set(); - - // All account_ids must be different - 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 - 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 - ensure!( - witness_set.is_valid_for(message), - NssaError::InvalidInput("Invalid signature for given message and public key".into()) - ); - - let signer_account_ids = tx.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; - ensure!( - current_nonce == *nonce, - NssaError::InvalidInput("Nonce mismatch".into()) - ); - } - - // Build pre_states for execution - let input_pre_states: Vec<_> = message - .account_ids - .iter() - .map(|account_id| { - AccountWithMetadata::new( - state.get_account_by_id(*account_id), - signer_account_ids.contains(account_id), - *account_id, - ) - }) - .collect(); - - let mut state_diff: HashMap = HashMap::new(); - - let initial_call = ChainedCall { - program_id: message.program_id, - instruction_data: message.instruction_data.clone(), - pre_states: input_pre_states, - pda_seeds: vec![], - }; - - let mut chained_calls = VecDeque::from_iter([(initial_call, None)]); - let mut chain_calls_counter = 0; - - while let Some((chained_call, caller_program_id)) = chained_calls.pop_front() { - 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 { - return Err(NssaError::InvalidInput("Unknown program".into())); - }; - - debug!( - "Program {:?} pre_states: {:?}, instruction_data: {:?}", - chained_call.program_id, chained_call.pre_states, chained_call.instruction_data - ); - let mut program_output = program.execute( - caller_program_id, - &chained_call.pre_states, - &chained_call.instruction_data, - )?; - debug!( - "Program {:?} output: {:?}", - chained_call.program_id, program_output - ); - - let authorized_pdas = - compute_public_authorized_pdas(caller_program_id, &chained_call.pda_seeds); - - let is_authorized = |account_id: &AccountId| { - signer_account_ids.contains(account_id) || authorized_pdas.contains(account_id) - }; - - for pre in &program_output.pre_states { - let account_id = pre.account_id; - // Check that the program output pre_states coincide with the values in the public - // state or with any modifications to those values during the chain of calls. - let expected_pre = state_diff - .get(&account_id) - .cloned() - .unwrap_or_else(|| state.get_account_by_id(account_id)); - ensure!( - pre.account == expected_pre, - InvalidProgramBehaviorError::InconsistentAccountPreState { - account_id, - expected: Box::new(expected_pre), - actual: Box::new(pre.account.clone()) - } - ); - - // Check that authorization flags are consistent with the provided ones or - // authorized by program through the PDA mechanism - let expected_is_authorized = is_authorized(&account_id); - ensure!( - pre.is_authorized == expected_is_authorized, - InvalidProgramBehaviorError::InconsistentAccountAuthorization { - account_id, - expected_authorization: expected_is_authorized, - actual_authorization: pre.is_authorized - } - ); - } - - // Verify that the program output's self_program_id matches the expected program ID. - ensure!( - program_output.self_program_id == chained_call.program_id, - InvalidProgramBehaviorError::MismatchedProgramId { - expected: chained_call.program_id, - actual: program_output.self_program_id - } - ); - - // Verify that the program output's caller_program_id matches the actual caller. - ensure!( - program_output.caller_program_id == caller_program_id, - InvalidProgramBehaviorError::MismatchedCallerProgramId { - expected: caller_program_id, - actual: program_output.caller_program_id, - } - ); - - // Verify execution corresponds to a well-behaved program. - // See the # Programs section for the definition of the `validate_execution` method. - validate_execution( - &program_output.pre_states, - &program_output.post_states, - chained_call.program_id, - ) - .map_err(InvalidProgramBehaviorError::ExecutionValidationFailed)?; - - // Verify validity window - ensure!( - program_output.block_validity_window.is_valid_for(block_id) - && program_output - .timestamp_validity_window - .is_valid_for(timestamp), - NssaError::OutOfValidityWindow - ); - - for (i, post) in program_output.post_states.iter_mut().enumerate() { - let Some(claim) = post.required_claim() else { - continue; - }; - let account_id = program_output.pre_states[i].account_id; - - // The invoked program can only claim accounts with default program id. - ensure!( - post.account().program_owner == DEFAULT_PROGRAM_ID, - InvalidProgramBehaviorError::ClaimedNonDefaultAccount { account_id } - ); - - match claim { - Claim::Authorized => { - // The program can only claim accounts that were authorized by the signer. - ensure!( - is_authorized(&account_id), - InvalidProgramBehaviorError::ClaimedUnauthorizedAccount { account_id } - ); - } - Claim::Pda(seed) => { - // The program can only claim accounts that correspond to the PDAs it is - // authorized to claim. The public-execution path only sees public - // accounts, so the public-PDA derivation is the correct formula here. - let pda = AccountId::for_public_pda(&chained_call.program_id, &seed); - ensure!( - account_id == pda, - InvalidProgramBehaviorError::MismatchedPdaClaim { - expected: pda, - actual: account_id - } - ); - } - } - - post.account_mut().program_owner = chained_call.program_id; - } - - // Update the state diff - for (pre, post) in program_output - .pre_states - .iter() - .zip(program_output.post_states.iter()) - { - state_diff.insert(pre.account_id, post.account().clone()); - } - - for new_call in program_output.chained_calls.into_iter().rev() { - chained_calls.push_front((new_call, Some(chained_call.program_id))); - } - - chain_calls_counter = chain_calls_counter - .checked_add(1) - .expect("we check the max depth at the beginning of the loop"); - } - - // Check that all modified uninitialized accounts where claimed - for (account_id, post) in state_diff.iter().filter_map(|(account_id, post)| { - let pre = state.get_account_by_id(*account_id); - if pre.program_owner != DEFAULT_PROGRAM_ID { - return None; - } - if pre == *post { - return None; - } - Some((*account_id, post)) - }) { - ensure!( - post.program_owner != DEFAULT_PROGRAM_ID, - InvalidProgramBehaviorError::DefaultAccountModifiedWithoutClaim { account_id } - ); - } + let validator = PublicTransactionValidator::new(tx, state, block_id, timestamp); + let state_diff = crate::public_transaction::execute(validator, tx, state)?; Ok(Self(StateDiff { - signer_account_ids, + signer_account_ids: tx.signer_account_ids(), public_diff: state_diff, new_commitments: vec![], new_nullifiers: vec![], @@ -293,63 +67,62 @@ impl ValidatedStateDiff { let witness_set = &tx.witness_set; // 1. Commitments or nullifiers are non empty - if message.new_commitments.is_empty() && message.new_nullifiers.is_empty() { - return Err(NssaError::InvalidInput( + ensure!( + !message.new_commitments.is_empty() || !message.new_nullifiers.is_empty(), + NssaError::InvalidInput( "Empty commitments and empty nullifiers found in message".into(), - )); - } + ) + ); // 2. Check there are no duplicate account_ids in the public_account_ids list. - if n_unique(&message.public_account_ids) != message.public_account_ids.len() { - return Err(NssaError::InvalidInput( - "Duplicate account_ids found in message".into(), - )); - } + ensure!( + n_unique(&message.public_account_ids) == message.public_account_ids.len(), + NssaError::InvalidInput("Duplicate account_ids found in message".into()) + ); // Check there are no duplicate nullifiers in the new_nullifiers list - if n_unique(&message.new_nullifiers) != message.new_nullifiers.len() { - return Err(NssaError::InvalidInput( - "Duplicate nullifiers found in message".into(), - )); - } + ensure!( + n_unique(&message.new_nullifiers) == message.new_nullifiers.len(), + NssaError::InvalidInput("Duplicate nullifiers found in message".into()) + ); // Check there are no duplicate commitments in the new_commitments list - if n_unique(&message.new_commitments) != message.new_commitments.len() { - return Err(NssaError::InvalidInput( - "Duplicate commitments found in message".into(), - )); - } + ensure!( + n_unique(&message.new_commitments) == message.new_commitments.len(), + NssaError::InvalidInput("Duplicate commitments found in message".into()) + ); // 3. Nonce checks and Valid signatures // 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.signatures_are_valid_for(message) { - return Err(NssaError::InvalidInput( - "Invalid signature for given message and public key".into(), - )); - } + ensure!( + witness_set.signatures_are_valid_for(message), + NssaError::InvalidInput("Invalid signature for given message and public key".into()) + ); let signer_account_ids = tx.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()) + ); } // Verify validity window - if !message.block_validity_window.is_valid_for(block_id) - || !message.timestamp_validity_window.is_valid_for(timestamp) - { - return Err(NssaError::OutOfValidityWindow); - } + ensure!( + message.block_validity_window.is_valid_for(block_id) + && message.timestamp_validity_window.is_valid_for(timestamp), + NssaError::OutOfValidityWindow + ); // Build pre_states for proof verification let public_pre_states: Vec<_> = message @@ -417,6 +190,22 @@ impl ValidatedStateDiff { })) } + pub fn from_public_genesis_transaction( + tx: &PublicTransaction, + state: &V03State, + ) -> Result { + let validator = GenesisPublicTransactionValidator; + let state_diff = crate::public_transaction::execute(validator, tx, state)?; + + Ok(Self(StateDiff { + signer_account_ids: tx.signer_account_ids(), + public_diff: state_diff, + new_commitments: vec![], + new_nullifiers: vec![], + program: None, + })) + } + /// Returns the public account changes produced by this transaction. /// /// Used by callers (e.g. the sequencer) to inspect the diff before committing it, for example @@ -431,6 +220,256 @@ impl ValidatedStateDiff { } } +pub struct PublicTransactionValidator<'tx, 'state> { + tx: &'tx PublicTransaction, + state: &'state V03State, + block_id: BlockId, + timestamp: Timestamp, + chain_calls_counter: usize, +} + +impl<'tx, 'state> PublicTransactionValidator<'tx, 'state> { + pub const fn new( + tx: &'tx PublicTransaction, + state: &'state V03State, + block_id: BlockId, + timestamp: Timestamp, + ) -> Self { + Self { + tx, + state, + block_id, + timestamp, + chain_calls_counter: 0, + } + } +} + +impl crate::public_transaction::Validator for PublicTransactionValidator<'_, '_> { + fn validate_pre_execution(&mut self) -> Result<(), NssaError> { + let message = self.tx.message(); + let witness_set = self.tx.witness_set(); + + // All account_ids must be different + 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 + 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 + ensure!( + witness_set.is_valid_for(message), + NssaError::InvalidInput("Invalid signature for given message and public key".into()) + ); + + let signer_account_ids = self.tx.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 = self.state.get_account_by_id(*account_id).nonce; + ensure!( + current_nonce == *nonce, + NssaError::InvalidInput("Nonce mismatch".into()) + ); + } + + Ok(()) + } + + fn on_chained_call(&mut self) -> Result<(), NssaError> { + self.chain_calls_counter = self + .chain_calls_counter + .checked_add(1) + .ok_or(NssaError::MaxChainedCallsDepthExceeded)?; + ensure!( + self.chain_calls_counter <= MAX_NUMBER_CHAINED_CALLS, + NssaError::MaxChainedCallsDepthExceeded + ); + Ok(()) + } + + fn validate_output( + &mut self, + state_diff: &HashMap, + caller_program_id: Option, + chained_call: &ChainedCall, + program_output: &ProgramOutput, + ) -> Result<(), NssaError> { + let authorized_pdas = + compute_public_authorized_pdas(caller_program_id, &chained_call.pda_seeds); + + let is_authorized = |account_id: &AccountId| { + self.tx.signer_account_ids().contains(account_id) + || authorized_pdas.contains(account_id) + }; + + for pre in &program_output.pre_states { + let account_id = pre.account_id; + // Check that the program output pre_states coincide with the values in the public + // state or with any modifications to those values during the chain of calls. + let expected_pre = state_diff + .get(&account_id) + .cloned() + .unwrap_or_else(|| self.state.get_account_by_id(account_id)); + ensure!( + pre.account == expected_pre, + InvalidProgramBehaviorError::InconsistentAccountPreState { + account_id, + expected: Box::new(expected_pre), + actual: Box::new(pre.account.clone()) + } + ); + + // Check that authorization flags are consistent with the provided ones or + // authorized by program through the PDA mechanism + let expected_is_authorized = is_authorized(&account_id); + ensure!( + pre.is_authorized == expected_is_authorized, + InvalidProgramBehaviorError::InconsistentAccountAuthorization { + account_id, + expected_authorization: expected_is_authorized, + actual_authorization: pre.is_authorized + } + ); + } + + // Verify that the program output's self_program_id matches the expected program ID. + ensure!( + program_output.self_program_id == chained_call.program_id, + InvalidProgramBehaviorError::MismatchedProgramId { + expected: chained_call.program_id, + actual: program_output.self_program_id + } + ); + + // Verify that the program output's caller_program_id matches the actual caller. + ensure!( + program_output.caller_program_id == caller_program_id, + InvalidProgramBehaviorError::MismatchedCallerProgramId { + expected: caller_program_id, + actual: program_output.caller_program_id, + } + ); + + // Verify execution corresponds to a well-behaved program. + // See the # Programs section for the definition of the `validate_execution` method. + validate_execution( + &program_output.pre_states, + &program_output.post_states, + chained_call.program_id, + ) + .map_err(InvalidProgramBehaviorError::ExecutionValidationFailed)?; + + // Verify validity window + ensure!( + program_output + .block_validity_window + .is_valid_for(self.block_id) + && program_output + .timestamp_validity_window + .is_valid_for(self.timestamp), + NssaError::OutOfValidityWindow + ); + + for (i, post) in program_output.post_states.iter().enumerate() { + let Some(claim) = post.required_claim() else { + continue; + }; + let account_id = program_output.pre_states[i].account_id; + + // The invoked program can only claim accounts with default program id. + ensure!( + post.account().program_owner == DEFAULT_PROGRAM_ID, + InvalidProgramBehaviorError::ClaimedNonDefaultAccount { account_id } + ); + + match claim { + Claim::Authorized => { + // The program can only claim accounts that were authorized by the signer. + ensure!( + is_authorized(&account_id), + InvalidProgramBehaviorError::ClaimedUnauthorizedAccount { account_id } + ); + } + Claim::Pda(seed) => { + // The program can only claim accounts that correspond to the PDAs it is + // authorized to claim. + let pda = AccountId::for_public_pda(&chained_call.program_id, &seed); + ensure!( + account_id == pda, + InvalidProgramBehaviorError::MismatchedPdaClaim { + expected: pda, + actual: account_id + } + ); + } + } + } + + Ok(()) + } + + fn validate_post_execution( + &mut self, + state_diff: &HashMap, + ) -> Result<(), NssaError> { + // Check that all modified uninitialized accounts where claimed + for (account_id, post) in state_diff.iter().filter_map(|(account_id, post)| { + let pre = self.state.get_account_by_id(*account_id); + if pre.program_owner != DEFAULT_PROGRAM_ID { + return None; + } + if pre == *post { + return None; + } + Some((*account_id, post)) + }) { + ensure!( + post.program_owner != DEFAULT_PROGRAM_ID, + InvalidProgramBehaviorError::DefaultAccountModifiedWithoutClaim { account_id } + ); + } + + Ok(()) + } +} + +pub struct GenesisPublicTransactionValidator; + +impl crate::public_transaction::Validator for GenesisPublicTransactionValidator { + fn validate_pre_execution(&mut self) -> Result<(), NssaError> { + Ok(()) + } + + fn on_chained_call(&mut self) -> Result<(), NssaError> { + Ok(()) + } + + fn validate_output( + &mut self, + _state_diff: &HashMap, + _caller_program_id: Option, + _chained_call: &ChainedCall, + _program_output: &ProgramOutput, + ) -> Result<(), NssaError> { + Ok(()) + } + + fn validate_post_execution( + &mut self, + _state_diff: &HashMap, + ) -> Result<(), NssaError> { + Ok(()) + } +} + fn check_privacy_preserving_circuit_proof_is_valid( proof: &Proof, public_pre_states: &[AccountWithMetadata], diff --git a/program_methods/guest/Cargo.toml b/program_methods/guest/Cargo.toml index dc2077b7..e0ef5e75 100644 --- a/program_methods/guest/Cargo.toml +++ b/program_methods/guest/Cargo.toml @@ -9,6 +9,7 @@ workspace = true [dependencies] nssa_core.workspace = true +authenticated_transfer_core.workspace = true clock_core.workspace = true token_core.workspace = true token_program.workspace = true diff --git a/program_methods/guest/src/bin/authenticated_transfer.rs b/program_methods/guest/src/bin/authenticated_transfer.rs index 32b69c3a..525fcf2b 100644 --- a/program_methods/guest/src/bin/authenticated_transfer.rs +++ b/program_methods/guest/src/bin/authenticated_transfer.rs @@ -1,3 +1,5 @@ +use authenticated_transfer_core::Instruction; +use clock_core::{CLOCK_01_PROGRAM_ACCOUNT_ID, ClockAccountData}; use nssa_core::{ account::{Account, AccountWithMetadata}, program::{ @@ -8,7 +10,6 @@ use nssa_core::{ /// Initializes a default account under the ownership of this program. fn initialize_account(pre_state: AccountWithMetadata) -> AccountPostState { let account_to_claim = AccountPostState::new_claimed(pre_state.account, Claim::Authorized); - let is_authorized = pre_state.is_authorized; // Continue only if the account to claim has default values assert!( @@ -16,9 +17,6 @@ fn initialize_account(pre_state: AccountWithMetadata) -> AccountPostState { "Account must be uninitialized" ); - // Continue only if the owner authorized this operation - assert!(is_authorized, "Account must be authorized"); - account_to_claim } @@ -61,6 +59,39 @@ fn transfer( vec![sender_post, recipient_post] } +/// Mints `balance` into a new account at genesis (`block_id` == 0). +/// +/// Claims the target account and sets its balance in a single operation. +fn mint( + target: AccountWithMetadata, + clock: AccountWithMetadata, + balance: u128, +) -> Vec { + assert_eq!( + clock.account_id, CLOCK_01_PROGRAM_ACCOUNT_ID, + "Second account must be the clock account" + ); + + let clock_data = ClockAccountData::from_bytes(&clock.account.data.clone().into_inner()); + assert_eq!( + clock_data.block_id, 0, + "Mint can only execute at genesis (block_id must be 0)" + ); + + assert!( + target.account == Account::default(), + "Target account must be uninitialized" + ); + + let mut target_post_account = target.account; + target_post_account.balance = balance; + let target_post = AccountPostState::new_claimed(target_post_account, Claim::Authorized); + + let clock_post = AccountPostState::new(clock.account); + + vec![target_post, clock_post] +} + /// A transfer of balance program. /// To be used both in public and private contexts. fn main() { @@ -70,20 +101,29 @@ fn main() { self_program_id, caller_program_id, pre_states, - instruction: balance_to_move, + instruction, }, instruction_words, - ) = read_nssa_inputs(); + ) = read_nssa_inputs::(); - let post_states = match (pre_states.as_slice(), balance_to_move) { - ([account_to_claim], 0) => { - let post = initialize_account(account_to_claim.clone()); - vec![post] + let post_states = match instruction { + Instruction::Initialize => { + let [account_to_claim] = <[_; 1]>::try_from(pre_states.clone()) + .expect("Initialize requires exactly 1 account"); + vec![initialize_account(account_to_claim)] } - ([sender, recipient], balance_to_move) => { - transfer(sender.clone(), recipient.clone(), balance_to_move) + Instruction::Transfer { + amount: balance_to_move, + } => { + let [sender, recipient] = <[_; 2]>::try_from(pre_states.clone()) + .expect("Transfer requires exactly 2 accounts"); + transfer(sender, recipient, balance_to_move) + } + Instruction::Mint { amount: balance } => { + let [target, clock] = <[_; 2]>::try_from(pre_states.clone()) + .expect("Mint requires exactly 2 accounts: target, clock"); + mint(target, clock, balance) } - _ => panic!("invalid params"), }; ProgramOutput::new( diff --git a/programs/authenticated_transfer/core/Cargo.toml b/programs/authenticated_transfer/core/Cargo.toml new file mode 100644 index 00000000..0331bd64 --- /dev/null +++ b/programs/authenticated_transfer/core/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "authenticated_transfer_core" +version = "0.1.0" +edition = "2024" +license = { workspace = true } + +[lints] +workspace = true + +[dependencies] +serde.workspace = true diff --git a/programs/authenticated_transfer/core/src/lib.rs b/programs/authenticated_transfer/core/src/lib.rs new file mode 100644 index 00000000..ed1121f8 --- /dev/null +++ b/programs/authenticated_transfer/core/src/lib.rs @@ -0,0 +1,29 @@ +//! Core data structures for the Authenticated Transfer Program. + +use serde::{Deserialize, Serialize}; + +/// Instruction type for the Authenticated Transfer program. +#[derive(Serialize, Deserialize)] +pub enum Instruction { + /// Transfer `amount` of native balance from sender to recipient. + /// + /// Required accounts: `[sender, recipient]`. + Transfer { amount: u128 }, + + /// Initialize a new account under the ownership of this program. + /// + /// Required accounts: `[account_to_initialize]`. + Initialize, + + /// Mint `amount` into a new account at genesis (`block_id` == 0). + /// + /// Claims the target account (sets `program_owner` to `authenticated_transfer` program id) + /// and sets its balance in a single operation. + /// + /// Required accounts: `[target_account, clock_account]`. + /// + /// Panics if: + /// - `target_account` is not in the default (uninitialized) state + /// - clock's `block_id` is not 0 + Mint { amount: u128 }, +} diff --git a/sequencer/core/Cargo.toml b/sequencer/core/Cargo.toml index 827c8b2e..ef9db98f 100644 --- a/sequencer/core/Cargo.toml +++ b/sequencer/core/Cargo.toml @@ -15,6 +15,7 @@ storage.workspace = true mempool.workspace = true logos-blockchain-zone-sdk.workspace = true testnet_initial_state.workspace = true +authenticated_transfer_core.workspace = true anyhow.workspace = true serde.workspace = true @@ -30,6 +31,7 @@ rand.workspace = true borsh.workspace = true bytesize.workspace = true url.workspace = true +rocksdb.workspace = true [features] default = [] diff --git a/sequencer/core/src/block_store.rs b/sequencer/core/src/block_store.rs index e85b5d33..ada6d306 100644 --- a/sequencer/core/src/block_store.rs +++ b/sequencer/core/src/block_store.rs @@ -6,9 +6,11 @@ use common::{ block::{Block, BlockMeta, MantleMsgId}, transaction::NSSATransaction, }; +use log::info; use logos_blockchain_zone_sdk::sequencer::SequencerCheckpoint; use nssa::V03State; -use storage::{error::DbError, sequencer::RocksDBIO}; +pub use storage::DbResult; +use storage::sequencer::RocksDBIO; pub struct SequencerStore { dbio: Arc, @@ -19,25 +21,53 @@ pub struct SequencerStore { } impl SequencerStore { + /// Open existing database at the given location. Fails if no database is found. + pub fn open_db(location: &Path, signing_key: nssa::PrivateKey) -> DbResult { + let dbio = Arc::new(RocksDBIO::open(location)?); + let genesis_id = dbio.get_meta_first_block_in_db()?; + let last_id = dbio.latest_block_meta()?.id; + + info!("Preparing block cache"); + let mut tx_hash_to_block_map = HashMap::new(); + for i in genesis_id..=last_id { + let block = dbio + .get_block(i)? + .expect("Block should be present in the database"); + + tx_hash_to_block_map.extend(block_to_transactions_map(&block)); + } + info!( + "Block cache prepared. Total blocks in cache: {}", + tx_hash_to_block_map.len() + ); + + Ok(Self { + dbio, + tx_hash_to_block_map, + genesis_id, + signing_key, + }) + } + /// Starting database at the start of new chain. /// Creates files if necessary. /// /// ATTENTION: Will overwrite genesis block. - pub fn open_db_with_genesis( + pub fn create_db_with_genesis( location: &Path, genesis_block: &Block, genesis_msg_id: MantleMsgId, + genesis_state: &V03State, signing_key: nssa::PrivateKey, - ) -> Result { - let tx_hash_to_block_map = block_to_transactions_map(genesis_block); - - let dbio = Arc::new(RocksDBIO::open_or_create( + ) -> DbResult { + let dbio = Arc::new(RocksDBIO::create( location, genesis_block, genesis_msg_id, + genesis_state, )?); - let genesis_id = dbio.get_meta_first_block_in_db()?; + let tx_hash_to_block_map = block_to_transactions_map(genesis_block); Ok(Self { dbio, @@ -55,16 +85,16 @@ impl SequencerStore { Arc::clone(&self.dbio) } - pub fn get_block_at_id(&self, id: u64) -> Result, DbError> { + pub fn get_block_at_id(&self, id: u64) -> DbResult> { self.dbio.get_block(id) } - pub fn delete_block_at_id(&mut self, block_id: u64) -> Result<()> { - Ok(self.dbio.delete_block(block_id)?) + pub fn delete_block_at_id(&mut self, block_id: u64) -> DbResult<()> { + self.dbio.delete_block(block_id) } - pub fn mark_block_as_finalized(&mut self, block_id: u64) -> Result<()> { - Ok(self.dbio.mark_block_as_finalized(block_id)?) + pub fn mark_block_as_finalized(&mut self, block_id: u64) -> DbResult<()> { + self.dbio.mark_block_as_finalized(block_id) } /// Returns the transaction corresponding to the given hash, if it exists in the blockchain. @@ -86,8 +116,8 @@ impl SequencerStore { ); } - pub fn latest_block_meta(&self) -> Result { - Ok(self.dbio.latest_block_meta()?) + pub fn latest_block_meta(&self) -> DbResult { + self.dbio.latest_block_meta() } #[must_use] @@ -100,8 +130,8 @@ impl SequencerStore { &self.signing_key } - pub fn get_all_blocks(&self) -> impl Iterator> { - self.dbio.get_all_blocks().map(|res| Ok(res?)) + pub fn get_all_blocks(&self) -> impl Iterator> { + self.dbio.get_all_blocks() } pub(crate) fn update( @@ -109,16 +139,15 @@ impl SequencerStore { block: &Block, msg_id: MantleMsgId, state: &V03State, - ) -> Result<()> { + ) -> DbResult<()> { let new_transactions_map = block_to_transactions_map(block); self.dbio.atomic_update(block, msg_id, state)?; self.tx_hash_to_block_map.extend(new_transactions_map); Ok(()) } - #[must_use] - pub fn get_nssa_state(&self) -> Option { - self.dbio.get_nssa_state().ok() + pub fn get_nssa_state(&self) -> DbResult { + self.dbio.get_nssa_state() } pub fn get_zone_checkpoint(&self) -> Result> { @@ -172,9 +201,14 @@ mod tests { let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]); // Start an empty node store - let mut node_store = - SequencerStore::open_db_with_genesis(path, &genesis_block, [0; 32], signing_key) - .unwrap(); + let mut node_store = SequencerStore::create_db_with_genesis( + path, + &genesis_block, + [0; 32], + &testnet_initial_state::initial_state(), + signing_key, + ) + .unwrap(); let tx = common::test_utils::produce_dummy_empty_transaction(); let block = common::test_utils::produce_dummy_block(1, None, vec![tx.clone()]); @@ -207,9 +241,14 @@ mod tests { let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]); let genesis_hash = genesis_block.header.hash; - let node_store = - SequencerStore::open_db_with_genesis(path, &genesis_block, [0; 32], signing_key) - .unwrap(); + let node_store = SequencerStore::create_db_with_genesis( + path, + &genesis_block, + [0; 32], + &testnet_initial_state::initial_state(), + signing_key, + ) + .unwrap(); // Verify that initially the latest block hash equals genesis hash let latest_meta = node_store.latest_block_meta().unwrap(); @@ -232,9 +271,14 @@ mod tests { }; let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]); - let mut node_store = - SequencerStore::open_db_with_genesis(path, &genesis_block, [0; 32], signing_key) - .unwrap(); + let mut node_store = SequencerStore::create_db_with_genesis( + path, + &genesis_block, + [0; 32], + &testnet_initial_state::initial_state(), + signing_key, + ) + .unwrap(); // Add a new block let tx = common::test_utils::produce_dummy_empty_transaction(); @@ -268,9 +312,14 @@ mod tests { }; let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]); - let mut node_store = - SequencerStore::open_db_with_genesis(path, &genesis_block, [0; 32], signing_key) - .unwrap(); + let mut node_store = SequencerStore::create_db_with_genesis( + path, + &genesis_block, + [0; 32], + &testnet_initial_state::initial_state(), + signing_key, + ) + .unwrap(); // Add a new block with Pending status let tx = common::test_utils::produce_dummy_empty_transaction(); @@ -297,4 +346,49 @@ mod tests { common::block::BedrockStatus::Finalized )); } + + #[test] + fn open_existing_db_caches_transactions() { + let temp_dir = tempdir().unwrap(); + let path = temp_dir.path(); + + let signing_key = sequencer_sign_key_for_testing(); + + let genesis_block_hashable_data = HashableBlockData { + block_id: 0, + prev_block_hash: HashType([0; 32]), + timestamp: 0, + transactions: vec![], + }; + + let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]); + let tx = common::test_utils::produce_dummy_empty_transaction(); + { + // Create a scope to drop the first store after creating the db + let mut node_store = SequencerStore::create_db_with_genesis( + path, + &genesis_block, + [0; 32], + &testnet_initial_state::initial_state(), + signing_key.clone(), + ) + .unwrap(); + + // Add a new block + let block = common::test_utils::produce_dummy_block(1, None, vec![tx.clone()]); + node_store + .update( + &block, + [1; 32], + &V03State::new_with_genesis_accounts(&[], vec![], 0), + ) + .unwrap(); + } + + // Re-open the store and verify that the transaction is still retrievable (which means it + // was cached correctly) + let node_store = SequencerStore::open_db(path, signing_key).unwrap(); + let retrieved_tx = node_store.get_transaction_by_hash(tx.hash()); + assert_eq!(Some(tx), retrieved_tx); + } } diff --git a/sequencer/core/src/config.rs b/sequencer/core/src/config.rs index b33dd694..bdac163b 100644 --- a/sequencer/core/src/config.rs +++ b/sequencer/core/src/config.rs @@ -10,17 +10,25 @@ use bytesize::ByteSize; use common::config::BasicAuth; use humantime_serde; use logos_blockchain_core::mantle::ops::channel::ChannelId; +use nssa::AccountId; use serde::{Deserialize, Serialize}; -use testnet_initial_state::{PrivateAccountPublicInitialData, PublicAccountPublicInitialData}; use url::Url; +/// A transaction to be applied at genesis to supply initial balances. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum GenesisTransaction { + SupplyPublicAccount { + account_id: AccountId, + balance: u128, + }, +} + // TODO: Provide default values #[derive(Clone, Serialize, Deserialize)] pub struct SequencerConfig { /// Home dir of sequencer storage. pub home: PathBuf, - /// Genesis id. - pub genesis_id: u64, /// If `True`, then adds random sequence of bytes to genesis block. pub is_genesis_random: bool, /// Maximum number of user transactions in a block (excludes the mandatory clock transaction). @@ -41,10 +49,9 @@ pub struct SequencerConfig { pub signing_key: [u8; 32], /// Bedrock configuration options. pub bedrock_config: BedrockConfig, - #[serde(skip_serializing_if = "Option::is_none")] - pub initial_public_accounts: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub initial_private_accounts: Option>, + /// Genesis configuration. + #[serde(default)] + pub genesis: Vec, } #[derive(Clone, Serialize, Deserialize)] diff --git a/sequencer/core/src/lib.rs b/sequencer/core/src/lib.rs index bce8151f..1c35aa8c 100644 --- a/sequencer/core/src/lib.rs +++ b/sequencer/core/src/lib.rs @@ -1,22 +1,22 @@ use std::{path::Path, time::Instant}; use anyhow::{Context as _, Result, anyhow}; -#[cfg(feature = "testnet")] -use common::PINATA_BASE58; use common::{ HashType, block::{BedrockStatus, Block, HashableBlockData}, transaction::{NSSATransaction, clock_invocation}, }; -use config::SequencerConfig; +use config::{GenesisTransaction, SequencerConfig}; use log::{error, info, warn}; use logos_blockchain_key_management_system_service::keys::{ED25519_SECRET_KEY_SIZE, Ed25519Key}; use mempool::{MemPool, MemPoolHandle}; #[cfg(feature = "mock")] pub use mock::SequencerCoreWithMockClients; -use nssa::V03State; +use nssa::{ + AccountId, PublicTransaction, ValidatedStateDiff, program::Program, public_transaction::Message, +}; +use nssa_core::GENESIS_BLOCK_ID; pub use storage::error::DbError; -use testnet_initial_state::initial_state; use crate::{ block_publisher::{BlockPublisherTrait, ZoneSdkPublisher}, @@ -48,34 +48,68 @@ impl SequencerCore { pub async fn start_from_config( config: SequencerConfig, ) -> (Self, MemPoolHandle) { - let hashable_data = HashableBlockData { - block_id: config.genesis_id, - transactions: vec![], - prev_block_hash: HashType([0; 32]), - timestamp: 0, - }; - let signing_key = nssa::PrivateKey::try_new(config.signing_key).unwrap(); - let genesis_parent_msg_id = [0; 32]; - let genesis_block = hashable_data.into_pending_block(&signing_key, genesis_parent_msg_id); + let db_path = config.home.join("rocksdb"); let bedrock_signing_key = load_or_create_signing_key(&config.home.join("bedrock_signing_key")) .expect("Failed to load or create bedrock signing key"); - // TODO: Remove msg_id from BlockMeta — it is no longer needed now that - // zone-sdk manages L1 settlement state via its own checkpoint. - let genesis_msg_id = [0_u8; 32]; + let (store, state, genesis_block) = + match SequencerStore::open_db(&db_path, signing_key.clone()) { + Ok(store) => { + let state = store + .get_nssa_state() + .expect("Failed to read state from store"); + let genesis_block = store + .get_block_at_id(store.genesis_id()) + .expect("Failed to read genesis block from store") + .expect("Genesis block not found in store"); + (store, state, genesis_block) + } + Err(DbError::RocksDbError { error, .. }) + if error.kind() == rocksdb::ErrorKind::InvalidArgument + && error.to_string().contains("does not exist") => + { + warn!( + "Database not found at {}, starting from genesis", + db_path.display() + ); + + // TODO: Remove msg_id from BlockMeta — it is no longer needed now that + // zone-sdk manages L1 settlement state via its own checkpoint. + let genesis_msg_id = [0; 32]; + let genesis_parent_msg_id = [0; 32]; + let (genesis_state, genesis_txs) = build_genesis_state(&config); + + let hashable_data = HashableBlockData { + block_id: GENESIS_BLOCK_ID, + transactions: genesis_txs, + prev_block_hash: HashType([0; 32]), + timestamp: 0, + }; + let genesis_block = + hashable_data.into_pending_block(&signing_key, genesis_parent_msg_id); + + let store = SequencerStore::create_db_with_genesis( + &db_path, + &genesis_block, + genesis_msg_id, + &genesis_state, + signing_key, + ) + .expect("Failed to create database with genesis block"); + + (store, genesis_state, genesis_block) + } + Err(err) => { + panic!( + "Failed to open database at {} with error: {err:#?}", + db_path.display() + ); + } + }; - // Sequencer should panic if unable to open db, - // as fixing this issue may require actions non-native to program scope - let store = SequencerStore::open_db_with_genesis( - &config.home.join("rocksdb"), - &genesis_block, - genesis_msg_id, - signing_key, - ) - .unwrap(); let latest_block_meta = store .latest_block_meta() .expect("Failed to read latest block meta from store"); @@ -125,63 +159,6 @@ impl SequencerCore { error!("Failed to publish genesis block: {err:#}"); } - #[cfg_attr(not(feature = "testnet"), allow(unused_mut))] - let mut state = if let Some(state) = store.get_nssa_state() { - info!("Found local database. Loading state and pending blocks from it."); - state - } else { - info!( - "No database found when starting the sequencer. Creating a fresh new with the initial data" - ); - - let initial_private_accounts: Option< - Vec<(nssa_core::Commitment, nssa_core::Nullifier)>, - > = config.initial_private_accounts.clone().map(|accounts| { - accounts - .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(&account_id, &acc), - nssa_core::Nullifier::for_account_initialization(&account_id), - ) - }) - .collect() - }); - - let init_accs: Option> = config - .initial_public_accounts - .clone() - .map(|initial_accounts| { - initial_accounts - .iter() - .map(|acc_data| (acc_data.account_id, acc_data.balance)) - .collect() - }); - - // If initial commitments or accounts are present in config, need to construct state - // from them - if initial_private_accounts.is_some() || init_accs.is_some() { - V03State::new_with_genesis_accounts( - &init_accs.unwrap_or_default(), - initial_private_accounts.unwrap_or_default(), - genesis_block.header.timestamp, - ) - } else { - initial_state() - } - }; - - #[cfg(feature = "testnet")] - state.add_pinata_program(PINATA_BASE58.parse().unwrap()); - let (mempool, mempool_handle) = MemPool::new(config.mempool_max_size); let sequencer_core = Self { @@ -359,7 +336,7 @@ impl SequencerCore { Ok(self .store .get_all_blocks() - .collect::>>()? + .collect::>>()? .into_iter() .filter(|block| matches!(block.bedrock_status, BedrockStatus::Pending)) .collect()) @@ -376,6 +353,61 @@ impl SequencerCore { } } +/// Builds the initial genesis state from `testnet_initial_state` plus configured genesis +/// transactions. Returns the final state and the list of [`NSSATransaction`]s that should be +/// committed to the genesis block so external observers can replay them. +fn build_genesis_state(config: &SequencerConfig) -> (nssa::V03State, Vec) { + #[cfg(not(feature = "testnet"))] + let mut state = testnet_initial_state::initial_state(); + + #[cfg(feature = "testnet")] + let mut state = testnet_initial_state::initial_state_testnet(); + + let mut genesis_txs = Vec::new(); + + for genesis_tx in &config.genesis { + let (tx, diff) = match genesis_tx { + GenesisTransaction::SupplyPublicAccount { + account_id, + balance, + } => build_supply_public_account_genesis_transaction(&state, account_id, *balance), + }; + state.apply_state_diff(diff); + genesis_txs.push(tx); + } + + let clock_tx = clock_invocation(0); + let diff = ValidatedStateDiff::from_public_transaction(&clock_tx, &state, GENESIS_BLOCK_ID, 0) + .expect("Failed to execute clock transaction for genesis block"); + state.apply_state_diff(diff); + genesis_txs.push(clock_tx.into()); + + (state, genesis_txs) +} + +fn build_supply_public_account_genesis_transaction( + state: &nssa::V03State, + account_id: &AccountId, + balance: u128, +) -> (NSSATransaction, ValidatedStateDiff) { + let authenticated_transfer_id = Program::authenticated_transfer_program().id(); + + let message = Message::try_new( + authenticated_transfer_id, + vec![*account_id, nssa::CLOCK_01_PROGRAM_ACCOUNT_ID], + vec![], + authenticated_transfer_core::Instruction::Mint { amount: balance }, + ) + .expect("Failed to serialize genesis mint instruction"); + let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]); + + let tx = PublicTransaction::new(message, witness_set); + let diff = ValidatedStateDiff::from_public_genesis_transaction(&tx, state) + .expect("Failed to execute genesis mint public transaction"); + + (tx.into(), diff) +} + /// Load signing key from file or generate a new one if it doesn't exist. fn load_or_create_signing_key(path: &Path) -> Result { if path.exists() { @@ -406,14 +438,19 @@ mod tests { use std::{pin::pin, time::Duration}; use common::{ + HashType, + block::HashableBlockData, test_utils::sequencer_sign_key_for_testing, transaction::{NSSATransaction, clock_invocation}, }; use logos_blockchain_core::mantle::ops::channel::ChannelId; use mempool::MemPoolHandle; + use tempfile::tempdir; use testnet_initial_state::{initial_accounts, initial_pub_accounts_private_keys}; use crate::{ + block_store::SequencerStore, + build_genesis_state, config::{BedrockConfig, SequencerConfig}, mock::SequencerCoreWithMockClients, }; @@ -424,7 +461,6 @@ mod tests { SequencerConfig { home, - genesis_id: 1, is_genesis_random: false, max_num_tx_in_block: 10, max_block_size: bytesize::ByteSize::mib(1), @@ -437,8 +473,7 @@ mod tests { auth: None, }, retry_pending_blocks_timeout: Duration::from_mins(4), - initial_public_accounts: None, - initial_private_accounts: None, + genesis: vec![], } } @@ -475,7 +510,7 @@ mod tests { let (sequencer, _mempool_handle) = SequencerCoreWithMockClients::start_from_config(config.clone()).await; - assert_eq!(sequencer.chain_height, config.genesis_id); + assert_eq!(sequencer.chain_height, 1); assert_eq!(sequencer.sequencer_config.max_num_tx_in_block, 10); let acc1_account_id = initial_accounts()[0].account_id; @@ -488,6 +523,57 @@ mod tests { assert_eq!(20000, balance_acc_2); } + #[tokio::test] + async fn start_from_config_opens_existing_db_if_it_exists() { + let config = setup_sequencer_config(); + let temp_dir = tempdir().unwrap(); + let mut config = config; + config.home = temp_dir.path().to_path_buf(); + + let signing_key = nssa::PrivateKey::try_new(config.signing_key).unwrap(); + let (genesis_state, genesis_txs) = build_genesis_state(&config); + let genesis_hashable_data = HashableBlockData { + block_id: 1, + transactions: genesis_txs, + prev_block_hash: HashType([0; 32]), + timestamp: 0, + }; + let genesis_block = genesis_hashable_data.into_pending_block(&signing_key, [0; 32]); + + let expected_msg_id = [7; 32]; + SequencerStore::create_db_with_genesis( + &config.home.join("rocksdb"), + &genesis_block, + expected_msg_id, + &genesis_state, + signing_key, + ) + .unwrap(); + + let (sequencer, _mempool_handle) = + SequencerCoreWithMockClients::start_from_config(config).await; + let latest_meta = sequencer.store.latest_block_meta().unwrap(); + + assert_eq!(latest_meta.msg_id, expected_msg_id); + assert_eq!(sequencer.chain_height, 1); + } + + #[should_panic(expected = "Failed to open database")] + #[tokio::test] + async fn start_from_config_panics_when_db_open_returns_non_not_found_error() { + let mut config = setup_sequencer_config(); + let temp_dir = tempdir().unwrap(); + config.home = temp_dir.path().to_path_buf(); + + let db_path = config.home.join("rocksdb"); + + std::fs::create_dir_all(&config.home).unwrap(); + // Force RocksDB open to fail with an IO error by placing a file at DB path. + std::fs::write(&db_path, b"not-a-directory").unwrap(); + + let _ = SequencerCoreWithMockClients::start_from_config(config).await; + } + #[test] fn transaction_pre_check_pass() { let tx = common::test_utils::produce_dummy_empty_transaction(); @@ -906,51 +992,6 @@ mod tests { ); } - #[tokio::test] - async fn start_from_config_uses_db_height_not_config_genesis() { - let mut config = setup_sequencer_config(); - let original_genesis_id = config.genesis_id; - - // Step 1: Create initial database and produce some blocks - let expected_chain_height = { - let (mut sequencer, mempool_handle) = - SequencerCoreWithMockClients::start_from_config(config.clone()).await; - - // Verify we start with the genesis_id from config - assert_eq!(sequencer.chain_height, original_genesis_id); - - // Produce multiple blocks to advance chain height - let tx = common::test_utils::produce_dummy_empty_transaction(); - mempool_handle.push(tx).await.unwrap(); - sequencer.produce_new_block().await.unwrap(); - - let tx = common::test_utils::produce_dummy_empty_transaction(); - mempool_handle.push(tx).await.unwrap(); - sequencer.produce_new_block().await.unwrap(); - - // Return the current chain height (should be genesis_id + 2) - sequencer.chain_height - }; - - // Step 2: Modify the config to have a DIFFERENT genesis_id - let different_genesis_id = original_genesis_id + 100; - config.genesis_id = different_genesis_id; - - // Step 3: Restart sequencer with the modified config (different genesis_id) - let (sequencer, _mempool_handle) = - SequencerCoreWithMockClients::start_from_config(config.clone()).await; - - // Step 4: Verify chain_height comes from database, NOT from the new config.genesis_id - assert_eq!( - sequencer.chain_height, expected_chain_height, - "Chain height should be loaded from database metadata, not config.genesis_id" - ); - assert_ne!( - sequencer.chain_height, different_genesis_id, - "Chain height should NOT match the modified config.genesis_id" - ); - } - #[tokio::test] async fn user_tx_that_chain_calls_clock_is_dropped() { let (mut sequencer, mempool_handle) = common_setup().await; @@ -1028,81 +1069,4 @@ mod tests { "Block production should abort when clock account data is corrupted" ); } - - #[tokio::test] - async fn genesis_private_account_cannot_be_re_initialized() { - use common::transaction::NSSATransaction; - use nssa::{ - Account, - privacy_preserving_transaction::{ - PrivacyPreservingTransaction, circuit::execute_and_prove, message::Message, - witness_set::WitnessSet, - }, - program::Program, - }; - use nssa_core::{ - InputAccountIdentity, SharedSecretKey, - account::AccountWithMetadata, - encryption::{EphemeralPublicKey, EphemeralSecretKey, ViewingPublicKey}, - }; - use testnet_initial_state::PrivateAccountPublicInitialData; - - let nsk: nssa_core::NullifierSecretKey = [7; 32]; - let npk = nssa_core::NullifierPublicKey::from(&nsk); - let vsk: EphemeralSecretKey = [8; 32]; - let vpk = ViewingPublicKey::from_scalar(vsk); - - let genesis_account = Account { - program_owner: Program::authenticated_transfer_program().id(), - ..Account::default() - }; - - // Start a sequencer from config with a preconfigured private genesis account - let mut config = setup_sequencer_config(); - config.initial_private_accounts = Some(vec![PrivateAccountPublicInitialData { - npk, - account: genesis_account, - }]); - - let (mut sequencer, _mempool_handle) = - SequencerCoreWithMockClients::start_from_config(config).await; - - // Attempt to re-initialize the same genesis account via a privacy-preserving transaction - let esk = [9; 32]; - let shared_secret = SharedSecretKey::new(&esk, &vpk); - let epk = EphemeralPublicKey::from_scalar(esk); - - let (output, proof) = execute_and_prove( - vec![AccountWithMetadata::new( - Account::default(), - true, - (&npk, 0), - )], - Program::serialize_instruction(0_u128).unwrap(), - vec![InputAccountIdentity::PrivateAuthorizedInit { - ssk: shared_secret, - nsk, - identifier: 0, - }], - &Program::authenticated_transfer_program().into(), - ) - .unwrap(); - - let message = - Message::try_from_circuit_output(vec![], vec![], vec![(npk, vpk, epk)], output) - .unwrap(); - - let witness_set = WitnessSet::for_message(&message, proof, &[]); - let tx = NSSATransaction::PrivacyPreserving(PrivacyPreservingTransaction::new( - message, - witness_set, - )); - - let result = tx.execute_check_on_state(&mut sequencer.state, 2, 0); - - assert!( - result.is_err_and(|e| e.to_string().contains("Nullifier already seen")), - "re-initializing a genesis private account must be rejected by the sequencer" - ); - } } diff --git a/storage/src/cells/shared_cells.rs b/storage/src/cells/shared_cells.rs index 2a76edf3..1efd0e35 100644 --- a/storage/src/cells/shared_cells.rs +++ b/storage/src/cells/shared_cells.rs @@ -63,6 +63,14 @@ impl SimpleStorableCell for FirstBlockCell { impl SimpleReadableCell for FirstBlockCell {} +impl SimpleWritableCell for FirstBlockCell { + fn value_constructor(&self) -> DbResult> { + borsh::to_vec(&self).map_err(|err| { + DbError::borsh_cast_message(err, Some("Failed to serialize first block id".to_owned())) + }) + } +} + #[derive(Debug, BorshSerialize, BorshDeserialize)] pub struct BlockCell(pub Block); diff --git a/storage/src/indexer/mod.rs b/storage/src/indexer/mod.rs index 75538835..4cc63c89 100644 --- a/storage/src/indexer/mod.rs +++ b/storage/src/indexer/mod.rs @@ -4,7 +4,7 @@ use common::{ block::Block, transaction::{NSSATransaction, clock_invocation}, }; -use nssa::V03State; +use nssa::{GENESIS_BLOCK_ID, V03State, ValidatedStateDiff}; use rocksdb::{ BoundColumnFamily, ColumnFamilyDescriptor, DBWithThreadMode, MultiThreaded, Options, }; @@ -56,11 +56,8 @@ impl DBIO for RocksDBIO { } impl RocksDBIO { - pub fn open_or_create( - path: &Path, - genesis_block: &Block, - initial_state: &V03State, - ) -> DbResult { + // TODO: Remove initial state when it will be included in genesis block + pub fn open_or_create(path: &Path, initial_state: &V03State) -> DbResult { let mut cf_opts = Options::default(); cf_opts.set_max_write_buffer_number(16); // ToDo: Add more column families for different data @@ -87,17 +84,9 @@ impl RocksDBIO { let dbio = Self { db }; - let is_start_set = dbio.get_meta_is_first_block_set()?; - if !is_start_set { - let block_id = genesis_block.header.block_id; - dbio.put_meta_last_block_in_db(block_id)?; - dbio.put_meta_first_block_in_db_batch(genesis_block)?; - dbio.put_meta_is_first_block_set()?; - - // First breakpoint setup - dbio.put_breakpoint(0, initial_state)?; - dbio.put_meta_last_breakpoint_id(0)?; - } + // First breakpoint setup + dbio.put_breakpoint(0, initial_state)?; + dbio.put_meta_last_breakpoint_id(0)?; Ok(dbio) } @@ -155,86 +144,107 @@ impl RocksDBIO { // State pub fn calculate_state_for_id(&self, block_id: u64) -> DbResult { - let last_block = self.get_meta_last_block_in_db()?; + let last_block_id = self.get_meta_last_block_id_in_db()?.unwrap_or(0); - if block_id <= last_block { - let br_id = closest_breakpoint_id(block_id); - let mut breakpoint = self.get_breakpoint(br_id)?; + if block_id > last_block_id { + return Err(DbError::db_interaction_error( + "Block on this id not found".to_owned(), + )); + } - // ToDo: update it to handle any genesis id - // right now works correctly only if genesis_id < BREAKPOINT_INTERVAL - let start = if br_id != 0 { - u64::from(BREAKPOINT_INTERVAL) - .checked_mul(br_id) - .expect("Reached maximum breakpoint id") - } else { - self.get_meta_first_block_in_db()? - }; + let br_id = closest_breakpoint_id(block_id); + let mut breakpoint = self.get_breakpoint(br_id)?; - for block in self.get_block_batch_seq( - start.checked_add(1).expect("Will be lesser that u64::MAX")..=block_id, - )? { - let expected_clock = - NSSATransaction::Public(clock_invocation(block.header.timestamp)); + let start = u64::from(BREAKPOINT_INTERVAL) + .checked_mul(br_id) + .expect("Reached maximum breakpoint id"); - if let Some((clock_tx, user_txs)) = block.body.transactions.split_last() { - if *clock_tx != expected_clock { - return Err(DbError::db_interaction_error( - "Last transaction in block must be the clock invocation for the block timestamp" - .to_owned(), - )); - } - for transaction in user_txs { - transaction - .clone() - .transaction_stateless_check() - .map_err(|err| { - DbError::db_interaction_error(format!( - "transaction pre check failed with err {err:?}" - )) - })? - .execute_check_on_state( - &mut breakpoint, - block.header.block_id, - block.header.timestamp, - ) - .map_err(|err| { - DbError::db_interaction_error(format!( - "transaction execution failed with err {err:?}" - )) - })?; - } + for mut block in self.get_block_batch_seq( + start.checked_add(1).expect("Will be lesser that u64::MAX")..=block_id, + )? { + let expected_clock = NSSATransaction::Public(clock_invocation(block.header.timestamp)); - let NSSATransaction::Public(clock_public_tx) = clock_tx else { - return Err(DbError::db_interaction_error( - "Clock invocation must be a public transaction".to_owned(), - )); + let clock_tx = block.body.transactions.pop().ok_or_else(|| { + DbError::db_interaction_error( + "Block must contain clock transaction at the end".to_owned(), + ) + })?; + let user_txs = block.body.transactions; + + if clock_tx != expected_clock { + return Err(DbError::db_interaction_error( + "Last transaction in block must be the clock invocation for the block timestamp" + .to_owned(), + )); + } + for transaction in user_txs { + let is_genesis = block.header.block_id == GENESIS_BLOCK_ID; + if is_genesis { + let genesis_tx = match transaction { + NSSATransaction::Public(public_tx) => public_tx, + NSSATransaction::PrivacyPreserving(_) + | NSSATransaction::ProgramDeployment(_) => { + return Err(DbError::db_interaction_error( + "Genesis block should contain only public transactions".to_owned(), + )); + } }; - - breakpoint - .transition_from_public_transaction( - clock_public_tx, + let state_diff = ValidatedStateDiff::from_public_genesis_transaction( + &genesis_tx, + &breakpoint, + ) + .map_err(|err| { + DbError::db_interaction_error(format!( + "Failed to create state diff from genesis transaction with err {err:?}" + )) + })?; + breakpoint.apply_state_diff(state_diff); + } else { + transaction + .transaction_stateless_check() + .map_err(|err| { + DbError::db_interaction_error(format!( + "transaction pre check failed with err {err:?}" + )) + })? + .execute_check_on_state( + &mut breakpoint, block.header.block_id, block.header.timestamp, ) .map_err(|err| { DbError::db_interaction_error(format!( - "clock transaction execution failed with err {err:?}" + "transaction execution failed with err {err:?}" )) })?; } } - Ok(breakpoint) - } else { - Err(DbError::db_interaction_error( - "Block on this id not found".to_owned(), - )) + let NSSATransaction::Public(clock_public_tx) = clock_tx else { + return Err(DbError::db_interaction_error( + "Clock invocation must be a public transaction".to_owned(), + )); + }; + + breakpoint + .transition_from_public_transaction( + &clock_public_tx, + block.header.block_id, + block.header.timestamp, + ) + .map_err(|err| { + DbError::db_interaction_error(format!( + "clock transaction execution failed with err {err:?}" + )) + })?; } + + Ok(breakpoint) } pub fn final_state(&self) -> DbResult { - self.calculate_state_for_id(self.get_meta_last_block_in_db()?) + let last_block_id = self.get_meta_last_block_id_in_db()?.unwrap_or(0); + self.calculate_state_for_id(last_block_id) } } @@ -255,7 +265,7 @@ mod tests { use super::*; fn genesis_block() -> Block { - common::test_utils::produce_dummy_block(1, None, vec![]) + produce_dummy_block(1, None, vec![]) } fn acc1_sign_key() -> nssa::PrivateKey { @@ -281,7 +291,6 @@ mod tests { let dbio = RocksDBIO::open_or_create( temdir_path, - &genesis_block(), &nssa::V03State::new_with_genesis_accounts( &[(acc1(), 10000), (acc2(), 20000)], vec![], @@ -290,21 +299,21 @@ mod tests { ) .unwrap(); - let last_id = dbio.get_meta_last_block_in_db().unwrap(); - let first_id = dbio.get_meta_first_block_in_db().unwrap(); + let last_id = dbio.get_meta_last_block_id_in_db().unwrap(); + let first_id = dbio.get_meta_first_block_id_in_db().unwrap(); let is_first_set = dbio.get_meta_is_first_block_set().unwrap(); let last_observed_l1_header = dbio.get_meta_last_observed_l1_lib_header_in_db().unwrap(); let last_br_id = dbio.get_meta_last_breakpoint_id().unwrap(); - let last_block = dbio.get_block(1).unwrap().unwrap(); + let last_block = dbio.get_block(1).unwrap(); let breakpoint = dbio.get_breakpoint(0).unwrap(); let final_state = dbio.final_state().unwrap(); - assert_eq!(last_id, 1); - assert_eq!(first_id, 1); + assert_eq!(last_id, None); + assert_eq!(first_id, None); assert_eq!(last_observed_l1_header, None); - assert!(is_first_set); - assert_eq!(last_br_id, 0); - assert_eq!(last_block.header.hash, genesis_block().header.hash); + assert!(!is_first_set); + assert_eq!(last_br_id, Some(0)); // TODO: Will be None after we remove hardcoded testnet state + assert!(last_block.is_none()); assert_eq!( breakpoint.get_account_by_id(acc1()), final_state.get_account_by_id(acc1()) @@ -322,7 +331,6 @@ mod tests { let dbio = RocksDBIO::open_or_create( temdir_path, - &genesis_block(), &nssa::V03State::new_with_genesis_accounts( &[(acc1(), 10000), (acc2(), 20000)], vec![], @@ -331,7 +339,10 @@ mod tests { ) .unwrap(); - let prev_hash = genesis_block().header.hash; + let genesis_block = genesis_block(); + dbio.put_block(&genesis_block, [0; 32]).unwrap(); + + let prev_hash = genesis_block.header.hash; let from = acc1(); let to = acc2(); let sign_key = acc1_sign_key(); @@ -342,8 +353,8 @@ mod tests { dbio.put_block(&block, [1; 32]).unwrap(); - let last_id = dbio.get_meta_last_block_in_db().unwrap(); - let first_id = dbio.get_meta_first_block_in_db().unwrap(); + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); + let first_id = dbio.get_meta_first_block_id_in_db().unwrap(); let last_observed_l1_header = dbio .get_meta_last_observed_l1_lib_header_in_db() .unwrap() @@ -355,11 +366,11 @@ mod tests { let final_state = dbio.final_state().unwrap(); assert_eq!(last_id, 2); - assert_eq!(first_id, 1); + assert_eq!(first_id, Some(1)); assert_eq!(last_observed_l1_header, [1; 32]); assert!(is_first_set); - assert_eq!(last_br_id, 0); - assert_ne!(last_block.header.hash, genesis_block().header.hash); + assert_eq!(last_br_id, Some(0)); + assert_eq!(last_block.header.hash, block.header.hash); assert_eq!( breakpoint.get_account_by_id(acc1()).balance - final_state.get_account_by_id(acc1()).balance, @@ -379,7 +390,6 @@ mod tests { let dbio = RocksDBIO::open_or_create( temdir_path, - &genesis_block(), &nssa::V03State::new_with_genesis_accounts( &[(acc1(), 10000), (acc2(), 20000)], vec![], @@ -392,11 +402,11 @@ mod tests { let to = acc2(); let sign_key = acc1_sign_key(); - for i in 1..=BREAKPOINT_INTERVAL { - let last_id = dbio.get_meta_last_block_in_db().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; + for i in 1..=BREAKPOINT_INTERVAL + 1 { + let prev_hash = dbio.get_meta_last_block_id_in_db().unwrap().map(|last_id| { + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + last_block.header.hash + }); let transfer_tx = common::test_utils::create_transaction_native_token_transfer( from, @@ -405,12 +415,12 @@ mod tests { 1, &sign_key, ); - let block = produce_dummy_block((i + 1).into(), Some(prev_hash), vec![transfer_tx]); + let block = produce_dummy_block(i.into(), prev_hash, vec![transfer_tx]); dbio.put_block(&block, [i; 32]).unwrap(); } - let last_id = dbio.get_meta_last_block_in_db().unwrap(); - let first_id = dbio.get_meta_first_block_in_db().unwrap(); + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); + let first_id = dbio.get_meta_first_block_id_in_db().unwrap(); let is_first_set = dbio.get_meta_is_first_block_set().unwrap(); let last_br_id = dbio.get_meta_last_breakpoint_id().unwrap(); let last_block = dbio.get_block(last_id).unwrap().unwrap(); @@ -419,19 +429,19 @@ mod tests { let final_state = dbio.final_state().unwrap(); assert_eq!(last_id, 101); - assert_eq!(first_id, 1); + assert_eq!(first_id, Some(1)); assert!(is_first_set); - assert_eq!(last_br_id, 1); + assert_eq!(last_br_id, Some(1)); assert_ne!(last_block.header.hash, genesis_block().header.hash); assert_eq!( prev_breakpoint.get_account_by_id(acc1()).balance - final_state.get_account_by_id(acc1()).balance, - 100 + 101 ); assert_eq!( final_state.get_account_by_id(acc2()).balance - prev_breakpoint.get_account_by_id(acc2()).balance, - 100 + 101 ); assert_eq!( breakpoint.get_account_by_id(acc1()).balance @@ -452,7 +462,6 @@ mod tests { let dbio = RocksDBIO::open_or_create( temdir_path, - &genesis_block(), &nssa::V03State::new_with_genesis_accounts( &[(acc1(), 10000), (acc2(), 20000)], vec![], @@ -465,31 +474,27 @@ mod tests { let to = acc2(); let sign_key = acc1_sign_key(); - let last_id = dbio.get_meta_last_block_in_db().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - 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 = produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]); + let block = produce_dummy_block(1, None, vec![transfer_tx]); let control_hash1 = block.header.hash; dbio.put_block(&block, [1; 32]).unwrap(); - let last_id = dbio.get_meta_last_block_in_db().unwrap(); + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); let last_block = dbio.get_block(last_id).unwrap().unwrap(); 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 = produce_dummy_block(3, Some(prev_hash), vec![transfer_tx]); + let block = produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]); let control_hash2 = block.header.hash; dbio.put_block(&block, [2; 32]).unwrap(); - let last_id = dbio.get_meta_last_block_in_db().unwrap(); + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); let last_block = dbio.get_block(last_id).unwrap().unwrap(); let prev_hash = last_block.header.hash; @@ -498,10 +503,10 @@ mod tests { let control_tx_hash1 = transfer_tx.hash(); - let block = produce_dummy_block(4, Some(prev_hash), vec![transfer_tx]); + let block = produce_dummy_block(3, Some(prev_hash), vec![transfer_tx]); dbio.put_block(&block, [3; 32]).unwrap(); - let last_id = dbio.get_meta_last_block_in_db().unwrap(); + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); let last_block = dbio.get_block(last_id).unwrap().unwrap(); let prev_hash = last_block.header.hash; @@ -510,7 +515,7 @@ mod tests { let control_tx_hash2 = transfer_tx.hash(); - let block = produce_dummy_block(5, Some(prev_hash), vec![transfer_tx]); + let block = produce_dummy_block(4, 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(); @@ -524,10 +529,10 @@ mod tests { .unwrap() .unwrap(); - assert_eq!(control_block_id1, 2); - assert_eq!(control_block_id2, 3); - assert_eq!(control_block_id3, 4); - assert_eq!(control_block_id4, 5); + assert_eq!(control_block_id1, 1); + assert_eq!(control_block_id2, 2); + assert_eq!(control_block_id3, 3); + assert_eq!(control_block_id4, 4); } #[test] @@ -539,7 +544,6 @@ mod tests { let dbio = RocksDBIO::open_or_create( temdir_path, - &genesis_block(), &nssa::V03State::new_with_genesis_accounts( &[(acc1(), 10000), (acc2(), 20000)], vec![], @@ -552,56 +556,52 @@ mod tests { let to = acc2(); let sign_key = acc1_sign_key(); - let last_id = dbio.get_meta_last_block_in_db().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - 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 = produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]); + let block = produce_dummy_block(1, None, vec![transfer_tx]); block_res.push(block.clone()); dbio.put_block(&block, [1; 32]).unwrap(); - let last_id = dbio.get_meta_last_block_in_db().unwrap(); + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); let last_block = dbio.get_block(last_id).unwrap().unwrap(); 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 = produce_dummy_block(3, 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, [2; 32]).unwrap(); - let last_id = dbio.get_meta_last_block_in_db().unwrap(); + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); let last_block = dbio.get_block(last_id).unwrap().unwrap(); let prev_hash = last_block.header.hash; let transfer_tx = common::test_utils::create_transaction_native_token_transfer(from, 2, to, 1, &sign_key); - let block = produce_dummy_block(4, 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, [3; 32]).unwrap(); - let last_id = dbio.get_meta_last_block_in_db().unwrap(); + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); let last_block = dbio.get_block(last_id).unwrap().unwrap(); let prev_hash = last_block.header.hash; let transfer_tx = common::test_utils::create_transaction_native_token_transfer(from, 3, to, 1, &sign_key); - let block = produce_dummy_block(5, 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, [4; 32]).unwrap(); let block_hashes_mem: Vec<[u8; 32]> = block_res.into_iter().map(|bl| bl.header.hash.0).collect(); - // 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(); + // Get blocks before ID 5 (i.e., starting from 4 going backwards), limit 4 + // This should return blocks 4, 3, 2, 1 in descending order + let mut batch_res = dbio.get_block_batch(Some(5), 4).unwrap(); batch_res.reverse(); // Reverse to match ascending order for comparison let block_hashes_db: Vec<[u8; 32]> = @@ -611,9 +611,9 @@ mod tests { let block_hashes_mem_limited = &block_hashes_mem[1..]; - // 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(); + // Get blocks before ID 5, limit 3 + // This should return blocks 4, 3, 2 in descending order + let mut batch_res_limited = dbio.get_block_batch(Some(5), 3).unwrap(); batch_res_limited.reverse(); // Reverse to match ascending order for comparison let block_hashes_db_limited: Vec<[u8; 32]> = batch_res_limited @@ -629,7 +629,7 @@ mod tests { .map(|block| block.header.block_id) .collect::>(); - assert_eq!(block_batch_ids, vec![1, 2, 3, 4, 5]); + assert_eq!(block_batch_ids, vec![1, 2, 3, 4]); } #[test] @@ -639,7 +639,6 @@ mod tests { let dbio = RocksDBIO::open_or_create( temdir_path, - &genesis_block(), &nssa::V03State::new_with_genesis_accounts( &[(acc1(), 10000), (acc2(), 20000)], vec![], @@ -654,10 +653,6 @@ mod tests { let mut tx_hash_res = vec![]; - let last_id = dbio.get_meta_last_block_in_db().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; let transfer_tx1 = common::test_utils::create_transaction_native_token_transfer(from, 0, to, 1, &sign_key); let transfer_tx2 = @@ -665,11 +660,11 @@ mod tests { tx_hash_res.push(transfer_tx1.hash().0); tx_hash_res.push(transfer_tx2.hash().0); - let block = produce_dummy_block(2, Some(prev_hash), vec![transfer_tx1, transfer_tx2]); + let block = produce_dummy_block(1, None, vec![transfer_tx1, transfer_tx2]); dbio.put_block(&block, [1; 32]).unwrap(); - let last_id = dbio.get_meta_last_block_in_db().unwrap(); + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); let last_block = dbio.get_block(last_id).unwrap().unwrap(); let prev_hash = last_block.header.hash; @@ -680,11 +675,11 @@ mod tests { tx_hash_res.push(transfer_tx1.hash().0); tx_hash_res.push(transfer_tx2.hash().0); - let block = produce_dummy_block(3, 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, [2; 32]).unwrap(); - let last_id = dbio.get_meta_last_block_in_db().unwrap(); + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); let last_block = dbio.get_block(last_id).unwrap().unwrap(); let prev_hash = last_block.header.hash; @@ -695,11 +690,11 @@ mod tests { tx_hash_res.push(transfer_tx1.hash().0); tx_hash_res.push(transfer_tx2.hash().0); - let block = produce_dummy_block(4, 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, [3; 32]).unwrap(); - let last_id = dbio.get_meta_last_block_in_db().unwrap(); + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); let last_block = dbio.get_block(last_id).unwrap().unwrap(); let prev_hash = last_block.header.hash; @@ -707,7 +702,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 = produce_dummy_block(5, Some(prev_hash), vec![transfer_tx]); + let block = produce_dummy_block(4, Some(prev_hash), vec![transfer_tx]); dbio.put_block(&block, [4; 32]).unwrap(); diff --git a/storage/src/indexer/read_multiple.rs b/storage/src/indexer/read_multiple.rs index 866fc7b0..d91e9627 100644 --- a/storage/src/indexer/read_multiple.rs +++ b/storage/src/indexer/read_multiple.rs @@ -12,7 +12,10 @@ impl RocksDBIO { before_id.saturating_sub(1) } else { // Get the latest block ID - self.get_meta_last_block_in_db()? + let Some(last) = self.get_meta_last_block_id_in_db()? else { + return Ok(vec![]); // No blocks in the database + }; + last }; for i in 0..limit { diff --git a/storage/src/indexer/read_once.rs b/storage/src/indexer/read_once.rs index 8ab7fd23..6e79adc4 100644 --- a/storage/src/indexer/read_once.rs +++ b/storage/src/indexer/read_once.rs @@ -12,12 +12,14 @@ use crate::{ impl RocksDBIO { // Meta - pub fn get_meta_first_block_in_db(&self) -> DbResult { - self.get::(()).map(|cell| cell.0) + pub fn get_meta_first_block_id_in_db(&self) -> DbResult> { + self.get_opt::(()) + .map(|opt| opt.map(|cell| cell.0)) } - pub fn get_meta_last_block_in_db(&self) -> DbResult { - self.get::(()).map(|cell| cell.0) + pub fn get_meta_last_block_id_in_db(&self) -> DbResult> { + self.get_opt::(()) + .map(|opt| opt.map(|cell| cell.0)) } pub fn get_meta_last_observed_l1_lib_header_in_db(&self) -> DbResult> { @@ -29,8 +31,9 @@ impl RocksDBIO { Ok(self.get_opt::(())?.is_some()) } - pub fn get_meta_last_breakpoint_id(&self) -> DbResult { - self.get::(()).map(|cell| cell.0) + pub fn get_meta_last_breakpoint_id(&self) -> DbResult> { + self.get_opt::(()) + .map(|opt| opt.map(|cell| cell.0)) } // Block diff --git a/storage/src/indexer/write_atomic.rs b/storage/src/indexer/write_atomic.rs index 9b661f3b..7e05791f 100644 --- a/storage/src/indexer/write_atomic.rs +++ b/storage/src/indexer/write_atomic.rs @@ -4,8 +4,8 @@ use rocksdb::WriteBatch; use super::{BREAKPOINT_INTERVAL, Block, DbError, DbResult, RocksDBIO}; use crate::{ - DB_META_FIRST_BLOCK_IN_DB_KEY, DBIO as _, - cells::shared_cells::{FirstBlockSetCell, LastBlockCell}, + DBIO as _, + cells::shared_cells::{FirstBlockCell, FirstBlockSetCell, LastBlockCell}, indexer::indexer_cells::{ AccNumTxCell, BlockHashToBlockIdMapCell, LastBreakpointIdCell, LastObservedL1LibHeaderCell, TxHashToBlockIdMapCell, @@ -143,28 +143,12 @@ impl RocksDBIO { // Meta - pub fn put_meta_first_block_in_db_batch(&self, block: &Block) -> DbResult<()> { - let cf_meta = self.meta_column(); - self.db - .put_cf( - &cf_meta, - borsh::to_vec(&DB_META_FIRST_BLOCK_IN_DB_KEY).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_META_FIRST_BLOCK_IN_DB_KEY".to_owned()), - ) - })?, - borsh::to_vec(&block.header.block_id).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize first block id".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - self.put_block(block, [0; 32])?; - Ok(()) + pub fn put_meta_first_block_in_db_batch( + &self, + block: &Block, + write_batch: &mut WriteBatch, + ) -> DbResult<()> { + self.put_batch(&FirstBlockCell(block.header.block_id), (), write_batch) } pub fn put_meta_last_block_in_db_batch( @@ -199,7 +183,7 @@ impl RocksDBIO { pub fn put_block(&self, block: &Block, l1_lib_header: [u8; 32]) -> DbResult<()> { let cf_block = self.block_column(); - let last_curr_block = self.get_meta_last_block_in_db()?; + let last_curr_block = self.get_meta_last_block_id_in_db()?.unwrap_or(0); let mut write_batch = WriteBatch::default(); write_batch.put_cf( @@ -216,6 +200,10 @@ impl RocksDBIO { self.put_meta_last_block_in_db_batch(block.header.block_id, &mut write_batch)?; self.put_meta_last_observed_l1_lib_header_in_db_batch(l1_lib_header, &mut write_batch)?; } + if last_curr_block == 0 { + self.put_meta_first_block_in_db_batch(block, &mut write_batch)?; + self.put_meta_is_first_block_set_batch(&mut write_batch)?; + } self.put_block_id_by_hash_batch( block.header.hash.into(), diff --git a/storage/src/indexer/write_non_atomic.rs b/storage/src/indexer/write_non_atomic.rs index 505360fa..7ddab1dd 100644 --- a/storage/src/indexer/write_non_atomic.rs +++ b/storage/src/indexer/write_non_atomic.rs @@ -42,9 +42,10 @@ impl RocksDBIO { } pub fn put_next_breakpoint(&self) -> DbResult<()> { - let last_block = self.get_meta_last_block_in_db()?; + let last_block = self.get_meta_last_block_id_in_db()?.unwrap_or(0); let next_breakpoint_id = self .get_meta_last_breakpoint_id()? + .unwrap_or(0) .checked_add(1) .expect("Breakpoint Id will be lesser than u64::MAX"); let block_to_break_id = next_breakpoint_id diff --git a/storage/src/sequencer/mod.rs b/storage/src/sequencer/mod.rs index 537d198d..be5e5cfe 100644 --- a/storage/src/sequencer/mod.rs +++ b/storage/src/sequencer/mod.rs @@ -42,36 +42,26 @@ impl DBIO for RocksDBIO { } impl RocksDBIO { - pub fn open_or_create( + pub fn open(path: &Path) -> DbResult { + let db_opts = Options::default(); + Self::open_inner(path, &db_opts) + } + + pub fn create( path: &Path, genesis_block: &Block, genesis_msg_id: MantleMsgId, + genesis_state: &V03State, ) -> DbResult { - let mut cf_opts = Options::default(); - cf_opts.set_max_write_buffer_number(16); - // ToDo: Add more column families for different data - let cfb = ColumnFamilyDescriptor::new(CF_BLOCK_NAME, cf_opts.clone()); - let cfmeta = ColumnFamilyDescriptor::new(CF_META_NAME, cf_opts.clone()); - let cfstate = ColumnFamilyDescriptor::new(CF_NSSA_STATE_NAME, cf_opts.clone()); - let mut db_opts = Options::default(); db_opts.create_missing_column_families(true); db_opts.create_if_missing(true); - let db = DBWithThreadMode::::open_cf_descriptors( - &db_opts, - path, - vec![cfb, cfmeta, cfstate], - ) - .map_err(|err| DbError::RocksDbError { - error: err, - additional_info: Some("Failed to open or create DB".to_owned()), - })?; - - let dbio = Self { db }; + let dbio = Self::open_inner(path, &db_opts)?; let is_start_set = dbio.get_meta_is_first_block_set()?; if !is_start_set { let block_id = genesis_block.header.block_id; + // TODO: Shouldn't this be atomic (batched)? dbio.put_meta_first_block_in_db(genesis_block, genesis_msg_id)?; dbio.put_meta_is_first_block_set()?; dbio.put_meta_last_block_in_db(block_id)?; @@ -81,11 +71,35 @@ impl RocksDBIO { hash: genesis_block.header.hash, msg_id: genesis_msg_id, })?; + dbio.put_nssa_state_in_db(genesis_state)?; } Ok(dbio) } + fn open_inner(path: &Path, db_opts: &Options) -> DbResult { + let mut cf_opts = Options::default(); + cf_opts.set_max_write_buffer_number(16); + + // ToDo: Add more column families for different data + let cfb = ColumnFamilyDescriptor::new(CF_BLOCK_NAME, cf_opts.clone()); + let cfmeta = ColumnFamilyDescriptor::new(CF_META_NAME, cf_opts.clone()); + let cfstate = ColumnFamilyDescriptor::new(CF_NSSA_STATE_NAME, cf_opts.clone()); + + let db = DBWithThreadMode::::open_cf_descriptors( + db_opts, + path, + vec![cfb, cfmeta, cfstate], + ) + .map_err(|err| DbError::RocksDbError { + error: err, + additional_info: Some("Failed to open or create DB".to_owned()), + })?; + + let dbio = Self { db }; + Ok(dbio) + } + pub fn destroy(path: &Path) -> DbResult<()> { let mut cf_opts = Options::default(); cf_opts.set_max_write_buffer_number(16); @@ -135,7 +149,15 @@ impl RocksDBIO { Ok(self.get_opt::(())?.is_some()) } - pub fn put_nssa_state_in_db(&self, state: &V03State, batch: &mut WriteBatch) -> DbResult<()> { + pub fn put_nssa_state_in_db(&self, state: &V03State) -> DbResult<()> { + self.put(&NSSAStateCellRef(state), ()) + } + + pub fn put_nssa_state_in_db_batch( + &self, + state: &V03State, + batch: &mut WriteBatch, + ) -> DbResult<()> { self.put_batch(&NSSAStateCellRef(state), (), batch) } @@ -366,7 +388,7 @@ impl RocksDBIO { let block_id = block.header.block_id; let mut batch = WriteBatch::default(); self.put_block(block, msg_id, false, &mut batch)?; - self.put_nssa_state_in_db(state, &mut batch)?; + self.put_nssa_state_in_db_batch(state, &mut batch)?; self.db.write(batch).map_err(|rerr| { DbError::rocksdb_cast_message( rerr, diff --git a/test_program_methods/guest/Cargo.toml b/test_program_methods/guest/Cargo.toml index 46edeb61..ca8cdc1d 100644 --- a/test_program_methods/guest/Cargo.toml +++ b/test_program_methods/guest/Cargo.toml @@ -9,6 +9,7 @@ workspace = true [dependencies] nssa_core.workspace = true +authenticated_transfer_core.workspace = true clock_core.workspace = true risc0-zkvm.workspace = true diff --git a/test_program_methods/guest/src/bin/chain_caller.rs b/test_program_methods/guest/src/bin/chain_caller.rs index 5c124bed..ac25301b 100644 --- a/test_program_methods/guest/src/bin/chain_caller.rs +++ b/test_program_methods/guest/src/bin/chain_caller.rs @@ -1,3 +1,4 @@ +use authenticated_transfer_core::Instruction as AuthTransferInstruction; use nssa_core::program::{ AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, ProgramOutput, read_nssa_inputs, @@ -25,7 +26,7 @@ fn main() { return; }; - let instruction_data = to_vec(&balance).unwrap(); + let instruction_data = to_vec(&AuthTransferInstruction::Transfer { amount: balance }).unwrap(); let mut running_recipient_pre = recipient_pre.clone(); let mut running_sender_pre = sender_pre.clone(); diff --git a/test_program_methods/guest/src/bin/flash_swap_callback.rs b/test_program_methods/guest/src/bin/flash_swap_callback.rs index 251833bb..ca596163 100644 --- a/test_program_methods/guest/src/bin/flash_swap_callback.rs +++ b/test_program_methods/guest/src/bin/flash_swap_callback.rs @@ -63,7 +63,10 @@ fn main() { // Mark the receiver as authorized since it will be PDA-authorized in this chained call. let mut receiver_authorized = receiver_pre.clone(); receiver_authorized.is_authorized = true; - let transfer_instruction = risc0_zkvm::serde::to_vec(&instruction.amount) + let transfer_instruction = + risc0_zkvm::serde::to_vec(&authenticated_transfer_core::Instruction::Transfer { + amount: instruction.amount, + }) .expect("transfer instruction serialization"); chained_calls.push(ChainedCall { diff --git a/test_program_methods/guest/src/bin/flash_swap_initiator.rs b/test_program_methods/guest/src/bin/flash_swap_initiator.rs index 27d1f317..c6a76ebd 100644 --- a/test_program_methods/guest/src/bin/flash_swap_initiator.rs +++ b/test_program_methods/guest/src/bin/flash_swap_initiator.rs @@ -123,7 +123,10 @@ fn main() { let mut vault_authorized = vault_pre.clone(); vault_authorized.is_authorized = true; let transfer_instruction = - risc0_zkvm::serde::to_vec(&amount_out).expect("transfer instruction serialization"); + risc0_zkvm::serde::to_vec(&authenticated_transfer_core::Instruction::Transfer { + amount: amount_out, + }) + .expect("transfer instruction serialization"); let call_1 = ChainedCall { program_id: token_program_id, pre_states: vec![vault_authorized, receiver_pre.clone()], diff --git a/test_program_methods/guest/src/bin/malicious_authorization_changer.rs b/test_program_methods/guest/src/bin/malicious_authorization_changer.rs index f7aba4a0..894f22bf 100644 --- a/test_program_methods/guest/src/bin/malicious_authorization_changer.rs +++ b/test_program_methods/guest/src/bin/malicious_authorization_changer.rs @@ -32,7 +32,8 @@ fn main() { ..sender.clone() }; - let instruction_data = to_vec(&balance).unwrap(); + let instruction_data = + to_vec(&authenticated_transfer_core::Instruction::Transfer { amount: balance }).unwrap(); let chained_call = ChainedCall { program_id: transfer_program_id, diff --git a/testnet_initial_state/src/lib.rs b/testnet_initial_state/src/lib.rs index f6f1e288..3e9e7af0 100644 --- a/testnet_initial_state/src/lib.rs +++ b/testnet_initial_state/src/lib.rs @@ -1,6 +1,7 @@ use common::PINATA_BASE58; use key_protocol::key_management::{ KeyChain, + key_tree::chain_index::ChainIndex, secret_holders::{PrivateKeyHolder, SecretSpendingKey}, }; use nssa::{Account, AccountId, Data, PrivateKey, PublicKey, V03State}; @@ -97,6 +98,7 @@ pub struct PublicAccountPrivateInitialData { pub struct PrivateAccountPrivateInitialData { pub account: nssa_core::account::Account, pub key_chain: KeyChain, + pub chain_index: Option, pub identifier: nssa_core::Identifier, } @@ -156,6 +158,7 @@ pub fn initial_priv_accounts_private_keys() -> Vec Vec FfiAccountListEntry { + account_id: FfiBytes32::from_account_id(account_id), + is_public: true, + }, + AccountIdWithPrivacy::Private(account_id) => FfiAccountListEntry { + account_id: FfiBytes32::from_account_id(account_id), + is_public: false, + }, + }) + .collect::>(); let count = entries.len(); @@ -508,3 +488,168 @@ pub unsafe extern "C" fn wallet_ffi_free_account_data(account: *mut FfiAccount) } } } + +/// Import a public account private key into wallet storage. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `private_key_hex`: Hex-encoded private key string +/// +/// # Returns +/// - `Success` on successful import +/// - Error code on failure +/// +/// # Safety +/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` +/// - `private_key_hex` must be a valid pointer to a null-terminated C string +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_import_public_account( + handle: *mut WalletHandle, + private_key_hex: *const c_char, +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + let private_key_hex = match c_str_to_string(private_key_hex, "private_key_hex") { + Ok(value) => value, + Err(e) => return e, + }; + + let private_key = match nssa::PrivateKey::from_str(&private_key_hex) { + Ok(value) => value, + Err(e) => { + print_error(format!("Invalid public account private key: {e}")); + return WalletFfiError::InvalidKeyValue; + } + }; + + let mut wallet = match wrapper.core.lock() { + Ok(w) => w, + Err(e) => { + print_error(format!("Failed to lock wallet: {e}")); + return WalletFfiError::InternalError; + } + }; + + wallet + .storage_mut() + .key_chain_mut() + .add_imported_public_account(private_key); + + match wallet.store_persistent_data() { + Ok(()) => WalletFfiError::Success, + Err(e) => { + print_error(format!("Failed to save wallet after public import: {e}")); + WalletFfiError::StorageError + } + } +} + +/// Import a private account keychain and account state into wallet storage. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `key_chain_json`: JSON-encoded `key_protocol::key_management::KeyChain` +/// - `chain_index`: Optional chain index string (for example `/0/1`, `NULL` if unknown) +/// - `identifier`: Identifier for this private account as little-endian u128 bytes +/// - `account_state_json`: JSON-encoded `wallet::account::HumanReadableAccount` +/// +/// # Returns +/// - `Success` on successful import +/// - Error code on failure +/// +/// # Safety +/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` +/// - `key_chain_json` must be a valid pointer to a null-terminated C string +/// - `identifier` must be a valid pointer to a `FfiU128` struct +/// - `account_state_json` must be a valid pointer to a null-terminated C string +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_import_private_account( + handle: *mut WalletHandle, + key_chain_json: *const c_char, + chain_index: *const c_char, + identifier: *const FfiU128, + account_state_json: *const c_char, +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + if identifier.is_null() { + print_error("Null pointer for identifier"); + return WalletFfiError::NullPointer; + } + + let key_chain_json = match c_str_to_string(key_chain_json, "key_chain_json") { + Ok(value) => value, + Err(e) => return e, + }; + + let account_state_json = match c_str_to_string(account_state_json, "account_state_json") { + Ok(value) => value, + Err(e) => return e, + }; + + let key_chain: KeyChain = match serde_json::from_str(&key_chain_json) { + Ok(value) => value, + Err(e) => { + print_error(format!("Invalid key chain JSON: {e}")); + return WalletFfiError::SerializationError; + } + }; + + let account_state: HumanReadableAccount = match serde_json::from_str(&account_state_json) { + Ok(value) => value, + Err(e) => { + print_error(format!("Invalid account state JSON: {e}")); + return WalletFfiError::SerializationError; + } + }; + + let account = nssa::Account::from(account_state); + + 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 = if chain_index.is_null() { + None + } else { + let chain_index_path = match c_str_to_string(chain_index, "chain_index") { + Ok(value) => value, + Err(e) => return e, + }; + + let parsed_chain_index = match ChainIndex::from_str(&chain_index_path) { + Ok(value) => value, + Err(e) => { + print_error(format!("Invalid chain index string: {e}")); + return WalletFfiError::InvalidTypeConversion; + } + }; + + Some(parsed_chain_index) + }; + + let identifier = u128::from_le_bytes(unsafe { (*identifier).data }); + + wallet + .storage_mut() + .key_chain_mut() + .add_imported_private_account(key_chain, chain_index, identifier, account); + + match wallet.store_persistent_data() { + Ok(()) => WalletFfiError::Success, + Err(e) => { + print_error(format!("Failed to save wallet after private import: {e}")); + WalletFfiError::StorageError + } + } +} diff --git a/wallet-ffi/src/error.rs b/wallet-ffi/src/error.rs index a8c345b5..6afbffcc 100644 --- a/wallet-ffi/src/error.rs +++ b/wallet-ffi/src/error.rs @@ -4,6 +4,7 @@ /// Error codes returned by FFI functions. #[repr(C)] +// #[must_use] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum WalletFfiError { /// Operation completed successfully. diff --git a/wallet-ffi/src/keys.rs b/wallet-ffi/src/keys.rs index 0471f255..b676ffab 100644 --- a/wallet-ffi/src/keys.rs +++ b/wallet-ffi/src/keys.rs @@ -116,12 +116,11 @@ 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, _identifier)) = - wallet.storage().user_data.get_private_account(account_id) - else { + let Some(acc) = wallet.storage().key_chain().private_account(account_id) else { print_error("Private account not found in wallet"); return WalletFfiError::AccountNotFound; }; + let key_chain = acc.key_chain; // NPK is a 32-byte array let npk_bytes = key_chain.nullifier_public_key.0; diff --git a/wallet-ffi/src/lib.rs b/wallet-ffi/src/lib.rs index d84bf5a3..16943d3e 100644 --- a/wallet-ffi/src/lib.rs +++ b/wallet-ffi/src/lib.rs @@ -26,9 +26,13 @@ reason = "TODO: fix later" )] -use std::sync::OnceLock; +use std::{ + ffi::{c_char, CStr}, + sync::OnceLock, +}; use ::wallet::ExecutionFailureKind; +use error::WalletFfiError; // Re-export public types for cbindgen pub use error::WalletFfiError as FfiError; use tokio::runtime::Handle; @@ -88,3 +92,20 @@ pub(crate) fn map_execution_error(e: ExecutionFailureKind) -> FfiError { _ => FfiError::InternalError, } } + +/// Helper to convert a C string to a Rust String. +fn c_str_to_string(ptr: *const c_char, name: &str) -> Result { + if ptr.is_null() { + print_error(format!("Null pointer for {name}")); + return Err(WalletFfiError::NullPointer); + } + + let c_str = unsafe { CStr::from_ptr(ptr) }; + match c_str.to_str() { + Ok(s) => Ok(s.to_owned()), + Err(e) => { + print_error(format!("Invalid UTF-8 in {name}: {e}")); + Err(WalletFfiError::InvalidUtf8) + } + } +} diff --git a/wallet-ffi/src/sync.rs b/wallet-ffi/src/sync.rs index 41031d06..5f7a4413 100644 --- a/wallet-ffi/src/sync.rs +++ b/wallet-ffi/src/sync.rs @@ -93,7 +93,7 @@ pub unsafe extern "C" fn wallet_ffi_get_last_synced_block( }; unsafe { - *out_block_id = wallet.last_synced_block; + *out_block_id = wallet.storage().last_synced_block(); } WalletFfiError::Success diff --git a/wallet-ffi/src/types.rs b/wallet-ffi/src/types.rs index 87c30315..b970a8d3 100644 --- a/wallet-ffi/src/types.rs +++ b/wallet-ffi/src/types.rs @@ -149,7 +149,7 @@ impl FfiBytes32 { /// Create from an `AccountId`. #[must_use] - pub const fn from_account_id(id: &nssa::AccountId) -> Self { + pub const fn from_account_id(id: nssa::AccountId) -> Self { Self { data: *id.value() } } } @@ -186,8 +186,8 @@ impl From for u128 { } } -impl From<&nssa::AccountId> for FfiBytes32 { - fn from(id: &nssa::AccountId) -> Self { +impl From for FfiBytes32 { + fn from(id: nssa::AccountId) -> Self { Self::from_account_id(id) } } diff --git a/wallet-ffi/src/wallet.rs b/wallet-ffi/src/wallet.rs index 93fc20aa..7aabaa2d 100644 --- a/wallet-ffi/src/wallet.rs +++ b/wallet-ffi/src/wallet.rs @@ -10,7 +10,7 @@ use std::{ use wallet::WalletCore; use crate::{ - block_on, + c_str_to_string, error::{print_error, WalletFfiError}, types::WalletHandle, }; @@ -60,23 +60,6 @@ fn c_str_to_path(ptr: *const c_char, name: &str) -> Result Result { - if ptr.is_null() { - print_error(format!("Null pointer for {name}")); - return Err(WalletFfiError::NullPointer); - } - - let c_str = unsafe { CStr::from_ptr(ptr) }; - match c_str.to_str() { - Ok(s) => Ok(s.to_owned()), - Err(e) => { - print_error(format!("Invalid UTF-8 in {name}: {e}")); - Err(WalletFfiError::InvalidUtf8) - } - } -} - /// Create a new wallet with fresh storage. /// /// This initializes a new wallet with a new seed derived from the password. @@ -212,7 +195,7 @@ pub unsafe extern "C" fn wallet_ffi_save(handle: *mut WalletHandle) -> WalletFfi } }; - match block_on(wallet.store_persistent_data()) { + match wallet.store_persistent_data() { Ok(()) => WalletFfiError::Success, Err(e) => { print_error(format!("Failed to save wallet: {e}")); diff --git a/wallet-ffi/wallet_ffi.h b/wallet-ffi/wallet_ffi.h index 89026950..adbb7b50 100644 --- a/wallet-ffi/wallet_ffi.h +++ b/wallet-ffi/wallet_ffi.h @@ -410,6 +410,50 @@ enum WalletFfiError wallet_ffi_get_account_private(struct WalletHandle *handle, */ void wallet_ffi_free_account_data(struct FfiAccount *account); +/** + * Import a public account private key into wallet storage. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `private_key_hex`: Hex-encoded private key string + * + * # Returns + * - `Success` on successful import + * - Error code on failure + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `private_key_hex` must be a valid pointer to a null-terminated C string + */ +enum WalletFfiError wallet_ffi_import_public_account(struct WalletHandle *handle, + const char *private_key_hex); + +/** + * Import a private account keychain and account state into wallet storage. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `key_chain_json`: JSON-encoded `key_protocol::key_management::KeyChain` + * - `chain_index`: Optional chain index string (for example `/0/1`, `NULL` if unknown) + * - `identifier`: Identifier for this private account as little-endian u128 bytes + * - `account_state_json`: JSON-encoded `wallet::account::HumanReadableAccount` + * + * # Returns + * - `Success` on successful import + * - Error code on failure + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `key_chain_json` must be a valid pointer to a null-terminated C string + * - `identifier` must be a valid pointer to a `FfiU128` struct + * - `account_state_json` must be a valid pointer to a null-terminated C string + */ +enum WalletFfiError wallet_ffi_import_private_account(struct WalletHandle *handle, + const char *key_chain_json, + const char *chain_index, + const struct FfiU128 *identifier, + const char *account_state_json); + /** * Get the public key for a public account. * diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index 4e98b8ef..ed6fc1c5 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -11,6 +11,7 @@ workspace = true nssa_core.workspace = true nssa.workspace = true common.workspace = true +authenticated_transfer_core.workspace = true key_protocol.workspace = true sequencer_service_rpc = { workspace = true, features = ["client"] } token_core.workspace = true @@ -39,3 +40,9 @@ async-stream.workspace = true indicatif = { version = "0.18.3", features = ["improved_unicode"] } optfield = "0.4.0" url.workspace = true +derive_more = { workspace = true, features = ["display"] } + +[dev-dependencies] +tempfile.workspace = true +key_protocol = { workspace = true, features = ["test_utils"] } +bincode.workspace = true diff --git a/wallet/configs/debug/wallet_config.json b/wallet/configs/debug/wallet_config.json index 94e13ebd..926ee298 100644 --- a/wallet/configs/debug/wallet_config.json +++ b/wallet/configs/debug/wallet_config.json @@ -3,411 +3,5 @@ "seq_poll_timeout": "30s", "seq_tx_poll_max_blocks": 15, "seq_poll_max_retries": 10, - "seq_block_poll_max_amount": 100, - "initial_accounts": [ - { - "Public": { - "account_id": "CbgR6tj5kWx5oziiFptM7jMvrQeYY3Mzaao6ciuhSr2r", - "pub_sign_key": "7f273098f25b71e6c005a9519f2678da8d1c7f01f6a27778e2d9948abdf901fb" - } - }, - { - "Public": { - "account_id": "2RHZhw9h534Zr3eq2RGhQete2Hh667foECzXPmSkGni2", - "pub_sign_key": "f434f8741720014586ae43356d2aec6257da086222f604ddb75d69733b86fc4c" - } - }, - { - "Private": { - "account_id": "GoKB6RuE6pT2KxCqDXQqiCuuuYZaGdJNfctzyqRdGBCy", - "identifier": 0, - "account": { - "program_owner": [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - "balance": 10000, - "data": [], - "nonce": 0 - }, - "key_chain": { - "secret_spending_key": [ - 75, - 231, - 144, - 165, - 5, - 36, - 183, - 237, - 190, - 227, - 238, - 13, - 132, - 39, - 114, - 228, - 172, - 82, - 119, - 164, - 233, - 132, - 130, - 224, - 201, - 90, - 200, - 156, - 108, - 199, - 56, - 22 - ], - "private_key_holder": { - "nullifier_secret_key": [ - 212, - 34, - 166, - 184, - 182, - 77, - 127, - 176, - 147, - 68, - 148, - 190, - 41, - 244, - 8, - 202, - 51, - 10, - 44, - 43, - 93, - 41, - 229, - 130, - 54, - 96, - 198, - 242, - 10, - 227, - 119, - 1 - ], - "viewing_secret_key": [ - 205, - 10, - 5, - 19, - 148, - 98, - 49, - 19, - 251, - 186, - 247, - 216, - 75, - 53, - 184, - 36, - 84, - 87, - 236, - 205, - 105, - 217, - 213, - 21, - 61, - 183, - 133, - 174, - 121, - 115, - 51, - 203 - ] - }, - "nullifier_public_key": [ - 122, - 213, - 113, - 8, - 118, - 179, - 235, - 94, - 5, - 219, - 131, - 106, - 246, - 253, - 14, - 204, - 65, - 93, - 0, - 198, - 100, - 108, - 57, - 48, - 6, - 65, - 183, - 31, - 136, - 86, - 82, - 165 - ], - "viewing_public_key": [ - 3, - 165, - 235, - 215, - 77, - 4, - 19, - 45, - 0, - 27, - 18, - 26, - 11, - 226, - 126, - 174, - 144, - 167, - 160, - 199, - 14, - 23, - 49, - 163, - 49, - 138, - 129, - 229, - 79, - 9, - 15, - 234, - 30 - ] - } - } - }, - { - "Private": { - "account_id": "BCdMnPkdH2DrVhe7cGdawkPU9iapsSboRvJpWX8pWnLq", - "identifier": 0, - "account": { - "program_owner": [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - "balance": 20000, - "data": [], - "nonce": 0 - }, - "key_chain": { - "secret_spending_key": [ - 107, - 49, - 136, - 174, - 162, - 107, - 250, - 105, - 252, - 146, - 166, - 197, - 163, - 132, - 153, - 222, - 68, - 17, - 87, - 101, - 22, - 113, - 88, - 97, - 180, - 203, - 139, - 18, - 28, - 62, - 51, - 149 - ], - "private_key_holder": { - "nullifier_secret_key": [ - 219, - 5, - 233, - 185, - 144, - 150, - 100, - 58, - 97, - 5, - 57, - 163, - 110, - 46, - 241, - 216, - 155, - 217, - 100, - 51, - 184, - 21, - 225, - 148, - 198, - 9, - 121, - 239, - 232, - 98, - 22, - 218 - ], - "viewing_secret_key": [ - 35, - 105, - 230, - 121, - 218, - 177, - 21, - 55, - 83, - 80, - 95, - 235, - 161, - 83, - 11, - 221, - 67, - 83, - 1, - 218, - 49, - 242, - 53, - 29, - 26, - 171, - 170, - 144, - 49, - 233, - 159, - 48 - ] - }, - "nullifier_public_key": [ - 33, - 68, - 229, - 154, - 12, - 235, - 210, - 229, - 236, - 144, - 126, - 122, - 58, - 107, - 36, - 58, - 243, - 128, - 174, - 197, - 141, - 137, - 162, - 190, - 155, - 234, - 94, - 156, - 218, - 34, - 13, - 221 - ], - "viewing_public_key": [ - 3, - 122, - 7, - 137, - 250, - 84, - 10, - 85, - 3, - 15, - 134, - 250, - 205, - 40, - 126, - 211, - 14, - 120, - 15, - 55, - 56, - 214, - 72, - 243, - 83, - 17, - 124, - 242, - 251, - 184, - 174, - 150, - 83 - ] - } - } - } - ] + "seq_block_poll_max_amount": 100 } \ No newline at end of file diff --git a/wallet/src/account.rs b/wallet/src/account.rs new file mode 100644 index 00000000..dca0a051 --- /dev/null +++ b/wallet/src/account.rs @@ -0,0 +1,149 @@ +use std::str::FromStr; + +use base58::{FromBase58 as _, ToBase58 as _}; +use derive_more::Display; +use nssa::AccountId; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Debug, Display, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[display("{_0}")] +pub struct Label(String); + +impl Label { + #[expect( + clippy::needless_pass_by_value, + reason = "Convenience for caller and negligible cost" + )] + #[must_use] + pub fn new(label: impl ToString) -> Self { + Self(label.to_string()) + } +} + +impl FromStr for Label { + type Err = std::convert::Infallible; + + fn from_str(s: &str) -> std::result::Result { + Ok(Self(s.to_owned())) + } +} + +impl From<&str> for Label { + fn from(value: &str) -> Self { + Self(value.to_owned()) + } +} + +impl From for Label { + fn from(value: String) -> Self { + Self(value) + } +} + +#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum AccountIdWithPrivacy { + #[display("Public/{_0}")] + Public(AccountId), + #[display("Private/{_0}")] + Private(AccountId), +} + +#[derive(Debug, Error)] +pub enum AccountIdWithPrivacyParseError { + #[error("Invalid format, expected 'Public/{{account_id}}' or 'Private/{{account_id}}'")] + InvalidFormat, + #[error("Invalid account id")] + InvalidAccountId(#[from] nssa_core::account::AccountIdError), +} + +impl FromStr for AccountIdWithPrivacy { + type Err = AccountIdWithPrivacyParseError; + + fn from_str(s: &str) -> Result { + if let Some(stripped) = s.strip_prefix("Public/") { + Ok(Self::Public(AccountId::from_str(stripped)?)) + } else if let Some(stripped) = s.strip_prefix("Private/") { + Ok(Self::Private(AccountId::from_str(stripped)?)) + } else { + Err(AccountIdWithPrivacyParseError::InvalidFormat) + } + } +} + +/// Human-readable representation of an account. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HumanReadableAccount { + balance: u128, + program_owner: String, + data: String, + nonce: u128, +} + +impl FromStr for HumanReadableAccount { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + serde_json::from_str(s).map_err(Into::into) + } +} + +impl std::fmt::Display for HumanReadableAccount { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let json = serde_json::to_string_pretty(self).map_err(|_err| std::fmt::Error)?; + write!(f, "{json}") + } +} + +impl From for HumanReadableAccount { + fn from(account: nssa::Account) -> Self { + 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, + data, + nonce: account.nonce.0, + } + } +} + +impl From for nssa::Account { + fn from(account: HumanReadableAccount) -> Self { + let mut program_owner_bytes = [0_u8; 32]; + let decoded_program_owner = account + .program_owner + .from_base58() + .expect("Invalid base58 in HumanReadableAccount.program_owner"); + assert!( + decoded_program_owner.len() == 32, + "HumanReadableAccount.program_owner must decode to exactly 32 bytes" + ); + program_owner_bytes.copy_from_slice(&decoded_program_owner); + + let mut program_owner = [0_u32; 8]; + for (index, chunk) in program_owner_bytes.chunks_exact(4).enumerate() { + let chunk: [u8; 4] = chunk + .try_into() + .expect("chunk length is guaranteed to be 4"); + program_owner[index] = u32::from_le_bytes(chunk); + } + + let data = hex::decode(&account.data).expect("Invalid hex in HumanReadableAccount.data"); + let data = data + .try_into() + .expect("Invalid account data: exceeds maximum allowed size"); + + Self { + balance: account.balance, + program_owner, + data, + nonce: nssa_core::account::Nonce(account.nonce), + } + } +} diff --git a/wallet/src/chain_storage.rs b/wallet/src/chain_storage.rs deleted file mode 100644 index 8d168d8e..00000000 --- a/wallet/src/chain_storage.rs +++ /dev/null @@ -1,308 +0,0 @@ -use std::collections::{BTreeMap, HashMap, btree_map::Entry}; - -use anyhow::Result; -use bip39::Mnemonic; -use key_protocol::{ - key_management::{ - key_tree::{KeyTreePrivate, KeyTreePublic, chain_index::ChainIndex}, - secret_holders::SeedHolder, - }, - key_protocol_core::{NSSAUserData, UserPrivateAccountData}, -}; -use log::debug; -use nssa::program::Program; - -use crate::config::{InitialAccountData, Label, PersistentAccountData, WalletConfig}; - -pub struct WalletChainStore { - pub user_data: NSSAUserData, - pub wallet_config: WalletConfig, - pub labels: HashMap, -} - -impl WalletChainStore { - #[expect( - clippy::wildcard_enum_match_arm, - reason = "We perform search for specific variants only" - )] - pub fn new( - config: WalletConfig, - persistent_accounts: Vec, - labels: HashMap, - ) -> Result { - if persistent_accounts.is_empty() { - anyhow::bail!("Roots not found; please run setup beforehand"); - } - - let mut public_init_acc_map = BTreeMap::new(); - let mut private_init_acc_map = BTreeMap::new(); - - let public_root = persistent_accounts - .iter() - .find(|data| match data { - &PersistentAccountData::Public(data) => data.chain_index == ChainIndex::root(), - _ => false, - }) - .cloned() - .expect("Malformed persistent account data, must have public root"); - - let private_root = persistent_accounts - .iter() - .find(|data| match data { - &PersistentAccountData::Private(data) => data.chain_index == ChainIndex::root(), - _ => false, - }) - .cloned() - .expect("Malformed persistent account data, must have private root"); - - let mut public_tree = KeyTreePublic::new_from_root(match public_root { - PersistentAccountData::Public(data) => data.data, - _ => unreachable!(), - }); - let mut private_tree = KeyTreePrivate::new_from_root(match private_root { - PersistentAccountData::Private(data) => data.data, - _ => unreachable!(), - }); - - for pers_acc_data in persistent_accounts { - match pers_acc_data { - PersistentAccountData::Public(data) => { - public_tree.insert(data.account_id, data.chain_index, data.data); - } - PersistentAccountData::Private(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(), - UserPrivateAccountData { - key_chain: data.key_chain, - accounts: vec![(data.identifier, data.account)], - }, - ); - } - }, - } - } - - Ok(Self { - user_data: NSSAUserData::new_with_accounts( - public_init_acc_map, - private_init_acc_map, - public_tree, - private_tree, - )?, - wallet_config: config, - labels, - }) - } - - pub fn new_storage(config: WalletConfig, password: &str) -> Result<(Self, Mnemonic)> { - let mut public_init_acc_map = BTreeMap::new(); - let mut private_init_acc_map = BTreeMap::new(); - - let initial_accounts = config - .initial_accounts - .clone() - .unwrap_or_else(InitialAccountData::create_initial_accounts_data); - - for init_acc_data in initial_accounts { - match init_acc_data { - InitialAccountData::Public(data) => { - 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( - account_id, - UserPrivateAccountData { - key_chain: data.key_chain, - accounts: vec![(data.identifier, account)], - }, - ); - } - } - } - - // TODO: Use password for storage encryption - let _ = password; - let (seed_holder, mnemonic) = SeedHolder::new_mnemonic(""); - let public_tree = KeyTreePublic::new(&seed_holder); - let private_tree = KeyTreePrivate::new(&seed_holder); - - Ok(( - Self { - user_data: NSSAUserData::new_with_accounts( - public_init_acc_map, - private_init_acc_map, - public_tree, - private_tree, - )?, - wallet_config: config, - labels: HashMap::new(), - }, - mnemonic, - )) - } - - /// Restore storage from an existing mnemonic phrase. - pub fn restore_storage( - config: WalletConfig, - mnemonic: &Mnemonic, - password: &str, - ) -> Result { - // TODO: Use password for storage encryption - let _ = password; - let seed_holder = SeedHolder::from_mnemonic(mnemonic, ""); - let public_tree = KeyTreePublic::new(&seed_holder); - let private_tree = KeyTreePrivate::new(&seed_holder); - - Ok(Self { - user_data: NSSAUserData::new_with_accounts( - BTreeMap::new(), - BTreeMap::new(), - public_tree, - private_tree, - )?, - wallet_config: config, - labels: HashMap::new(), - }) - } - - 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:?}"); - - // Update default accounts if present - if let Entry::Occupied(mut entry) = self - .user_data - .default_user_private_accounts - .entry(account_id) - { - 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; - } - - // 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 - .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 - .account_id_map - .insert(account_id, ci.clone()); - break; - } - } - } - } -} - -#[cfg(test)] -mod tests { - use key_protocol::key_management::key_tree::{ - keys_private::ChildKeysPrivate, keys_public::ChildKeysPublic, - }; - - use super::*; - use crate::config::{PersistentAccountDataPrivate, PersistentAccountDataPublic}; - - fn create_sample_wallet_config() -> WalletConfig { - WalletConfig { - sequencer_addr: "http://127.0.0.1".parse().unwrap(), - seq_poll_timeout: std::time::Duration::from_secs(12), - seq_tx_poll_max_blocks: 5, - seq_poll_max_retries: 10, - seq_block_poll_max_amount: 100, - basic_auth: None, - initial_accounts: None, - } - } - - fn create_sample_persistent_accounts() -> Vec { - let public_data = ChildKeysPublic::root([42; 64]); - let private_data = ChildKeysPrivate::root([47; 64]); - - vec![ - PersistentAccountData::Public(PersistentAccountDataPublic { - account_id: public_data.account_id(), - chain_index: ChainIndex::root(), - data: public_data, - }), - PersistentAccountData::Private(Box::new(PersistentAccountDataPrivate { - identifiers: vec![], - chain_index: ChainIndex::root(), - data: private_data, - })), - ] - } - - #[test] - fn new_initializes_correctly() { - let config = create_sample_wallet_config(); - let accs = create_sample_persistent_accounts(); - - let _ = WalletChainStore::new(config, accs, HashMap::new()).unwrap(); - } -} diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index 0e12e9a5..2d7f325b 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -1,19 +1,15 @@ use anyhow::{Context as _, Result}; use clap::Subcommand; use itertools::Itertools as _; -use key_protocol::key_management::key_tree::chain_index::ChainIndex; +use key_protocol::key_management::{KeyChain, key_tree::chain_index::ChainIndex}; use nssa::{Account, PublicKey, program::Program}; -use sequencer_service_rpc::RpcClient as _; +use nssa_core::Identifier; use token_core::{TokenDefinition, TokenHolding}; use crate::{ WalletCore, - cli::{SubcommandReturnValue, WalletSubcommand}, - config::Label, - helperfunctions::{ - AccountPrivacyKind, HumanReadableAccount, parse_addr_with_privacy_prefix, - resolve_id_or_label, - }, + account::{AccountIdWithPrivacy, HumanReadableAccount, Label}, + cli::{CliAccountMention, SubcommandReturnValue, WalletSubcommand}, }; /// Represents generic chain CLI subcommand. @@ -27,17 +23,9 @@ pub enum AccountSubcommand { /// Display keys (pk for public accounts, npk/vpk for private accounts). #[arg(short, long)] keys: bool, - /// Valid 32 byte base58 string with privacy prefix. - #[arg( - short, - long, - conflicts_with = "account_label", - required_unless_present = "account_label" - )] - account_id: Option, - /// Account label (alternative to --account-id). - #[arg(long, conflicts_with = "account_id")] - account_label: Option, + /// Either 32 byte base58 account id string with privacy prefix or a label. + #[arg(short, long)] + account_id: CliAccountMention, }, /// Produce new public or private account. #[command(subcommand)] @@ -53,21 +41,16 @@ pub enum AccountSubcommand { }, /// Set a label for an account. Label { - /// Valid 32 byte base58 string with privacy prefix. - #[arg( - short, - long, - conflicts_with = "account_label", - required_unless_present = "account_label" - )] - account_id: Option, - /// Account label (alternative to --account-id). - #[arg(long = "account-label", conflicts_with = "account_id")] - account_label: Option, + /// Either 32 byte base58 account id string with privacy prefix or a label. + #[arg(short, long)] + account_id: CliAccountMention, /// The label to assign to the account. #[arg(short, long)] - label: String, + label: Label, }, + /// Import external account. + #[command(subcommand)] + Import(ImportSubcommand), } /// Represents generic register CLI subcommand. @@ -80,7 +63,7 @@ pub enum NewSubcommand { cci: Option, #[arg(short, long)] /// Label to assign to the new account. - label: Option, + label: Option