diff --git a/.deny.toml b/.deny.toml index fb1ce3cf..59df488b 100644 --- a/.deny.toml +++ b/.deny.toml @@ -13,9 +13,9 @@ ignore = [ { id = "RUSTSEC-2025-0055", reason = "`tracing-subscriber` v0.2.25 pulled in by ark-relations v0.4.0 - will be addressed before mainnet" }, { 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-2025-0137", reason = "newest `rint` depends on rustc v1.90.0 and we build artifacts with v1.88.8" }, ] yanked = "deny" unused-ignored-advisory = "deny" diff --git a/.github/workflows/bench-regression.yml b/.github/workflows/bench-regression.yml new file mode 100644 index 00000000..994989bf --- /dev/null +++ b/.github/workflows/bench-regression.yml @@ -0,0 +1,44 @@ +on: + pull_request: + paths: + - "tools/crypto_primitives_bench/**" + - "key_protocol/**" + - "nssa/core/**" + - ".github/workflows/bench-regression.yml" + +permissions: + contents: read + pull-requests: write + +name: bench-regression + +jobs: + crypto-primitives: + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ github.event.pull_request.head.sha || github.head_ref }} + # criterion-compare-action checks out the base branch in a second + # working tree, so we need the full history. + fetch-depth: 0 + + - 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: Run criterion-compare against base branch + uses: boa-dev/criterion-compare-action@v3 + with: + branchName: ${{ github.base_ref }} + cwd: tools/crypto_primitives_bench + benchName: primitives + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7ed4f34..49ceaab9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,6 +94,12 @@ jobs: - name: Install active toolchain run: rustup install + - name: Restore Rust cache + uses: Swatinem/rust-cache@v2 + with: + shared-key: ci-rust-cache + save-if: ${{ github.ref == 'refs/heads/main' }} + - name: Lint workspace env: RISC0_SKIP_BUILD: "1" @@ -123,6 +129,12 @@ jobs: - name: Install active toolchain run: rustup install + - name: Restore Rust cache + uses: Swatinem/rust-cache@v2 + with: + shared-key: ci-rust-cache + save-if: ${{ github.ref == 'refs/heads/main' }} + - name: Install nextest run: cargo install --locked cargo-nextest @@ -132,9 +144,10 @@ jobs: RUST_LOG: "info" run: cargo nextest run --workspace --exclude integration_tests --all-features - integration-tests: + integration-tests-prebuild: runs-on: ubuntu-latest - timeout-minutes: 90 # TODO: Apply CI cache to speed this up + outputs: + targets: ${{ steps.discover-targets.outputs.targets }} steps: - uses: actions/checkout@v5 with: @@ -151,6 +164,75 @@ jobs: - name: Install active toolchain run: rustup install + - name: Restore Rust cache + uses: Swatinem/rust-cache@v2 + with: + shared-key: ci-rust-cache + save-if: ${{ github.ref == 'refs/heads/main' }} + + - name: Install nextest + run: cargo install --locked cargo-nextest + + - name: Build integration test archive + env: + RISC0_DEV_MODE: "1" + run: cargo nextest archive -p integration_tests --archive-file integration-tests.tar.zst --no-pager + + - name: Upload integration test archive + uses: actions/upload-artifact@v4 + with: + name: integration-tests-archive + path: integration-tests.tar.zst + + - name: Discover integration test targets from archive + id: discover-targets + run: | + cargo nextest list \ + --archive-file integration-tests.tar.zst \ + --list-type binaries-only \ + --message-format json \ + --no-pager > integration-tests-binaries.json + + targets_json="$(jq -c '[."rust-binaries" | to_entries[] | select(.value.kind == "test" and .value."binary-name" != "tps") | .value."binary-name"] | sort | unique' integration-tests-binaries.json)" + + if [[ "$targets_json" == "[]" ]]; then + echo "No integration test targets were discovered." >&2 + exit 1 + fi + + echo "targets=$targets_json" >> "$GITHUB_OUTPUT" + echo "Discovered integration targets: $targets_json" + + integration-tests: + needs: integration-tests-prebuild + runs-on: ubuntu-latest + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + target: ${{ fromJson(needs.integration-tests-prebuild.outputs.targets) }} + name: integration-tests (${{ matrix.target }}) + 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: Download integration test archive + uses: actions/download-artifact@v4 + with: + name: integration-tests-archive + - name: Install nextest run: cargo install --locked cargo-nextest @@ -158,7 +240,7 @@ jobs: env: RISC0_DEV_MODE: "1" RUST_LOG: "info" - run: cargo nextest run -p integration_tests -- --skip tps_test + run: cargo nextest run --archive-file integration-tests.tar.zst -E "binary(${{ matrix.target }})" valid-proof-test: runs-on: ubuntu-latest @@ -179,6 +261,12 @@ jobs: - name: Install active toolchain run: rustup install + - name: Restore Rust cache + uses: Swatinem/rust-cache@v2 + with: + shared-key: ci-rust-cache + save-if: ${{ github.ref == 'refs/heads/main' }} + - name: Test valid proof env: RUST_LOG: "info" @@ -196,6 +284,12 @@ jobs: - uses: ./.github/actions/install-risc0 + - name: Restore Rust cache + uses: Swatinem/rust-cache@v2 + with: + shared-key: ci-rust-cache + save-if: ${{ github.ref == 'refs/heads/main' }} + - name: Install just run: cargo install --locked just diff --git a/Cargo.lock b/Cargo.lock index 9b6b1597..5e02cf77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3646,7 +3646,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.3", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -7468,7 +7468,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.3", + "socket2 0.5.10", "thiserror 2.0.18", "tokio", "tracing", @@ -7505,7 +7505,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.3", + "socket2 0.5.10", "tracing", "windows-sys 0.60.2", ] @@ -8471,7 +8471,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs 0.26.11", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] diff --git a/artifacts/program_methods/amm.bin b/artifacts/program_methods/amm.bin index 3fbea6d0..52067453 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 e6cdba59..339f634c 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 0cdaf90d..ae1dc1aa 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 06b983ce..aaca2737 100644 Binary files a/artifacts/program_methods/clock.bin and b/artifacts/program_methods/clock.bin differ diff --git a/artifacts/program_methods/faucet.bin b/artifacts/program_methods/faucet.bin index c4a82241..8b3bda76 100644 Binary files a/artifacts/program_methods/faucet.bin and b/artifacts/program_methods/faucet.bin differ diff --git a/artifacts/program_methods/pinata.bin b/artifacts/program_methods/pinata.bin index d8634938..fad19f0b 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 adfd3cb6..bd38db1a 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 5aaae0ea..f8df6383 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 f11ca891..ba54f9f3 100644 Binary files a/artifacts/program_methods/token.bin and b/artifacts/program_methods/token.bin differ diff --git a/artifacts/program_methods/vault.bin b/artifacts/program_methods/vault.bin index 4bf1bfdf..1d853c3c 100644 Binary files a/artifacts/program_methods/vault.bin and b/artifacts/program_methods/vault.bin differ diff --git a/artifacts/test_program_methods/auth_asserting_noop.bin b/artifacts/test_program_methods/auth_asserting_noop.bin index 20a85c7c..720f8ea6 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 eb1cfff0..f79510cf 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 521149d9..d5c2611b 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 460209d4..edc15959 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 67f29a28..8871ce27 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 351680f5..2ca68617 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 e084b74a..b46141ce 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 5a4e11c2..4685608a 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 6ebf389c..3221ba35 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/faucet_chain_caller.bin b/artifacts/test_program_methods/faucet_chain_caller.bin index beeae731..81aa21f6 100644 Binary files a/artifacts/test_program_methods/faucet_chain_caller.bin and b/artifacts/test_program_methods/faucet_chain_caller.bin differ diff --git a/artifacts/test_program_methods/flash_swap_callback.bin b/artifacts/test_program_methods/flash_swap_callback.bin index 202b551c..513e7856 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 744c864b..3740635d 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 351e9ec6..b34e6398 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 29a8604a..c1f65dce 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_injector.bin b/artifacts/test_program_methods/malicious_injector.bin index 55ac856b..8c2acf82 100644 Binary files a/artifacts/test_program_methods/malicious_injector.bin and b/artifacts/test_program_methods/malicious_injector.bin differ diff --git a/artifacts/test_program_methods/malicious_launderer.bin b/artifacts/test_program_methods/malicious_launderer.bin index ec15275c..547a21a2 100644 Binary files a/artifacts/test_program_methods/malicious_launderer.bin and b/artifacts/test_program_methods/malicious_launderer.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 b10e1781..3a291460 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 5268710c..a27b7a4e 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 7b6de7b0..505322a6 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 41559320..aaa5dcb0 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 7d1ae5fa..8dc636da 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 63c1a0e3..69e69635 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 3c42374d..f5603d58 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/pda_fund_spend_proxy.bin b/artifacts/test_program_methods/pda_fund_spend_proxy.bin index 03e85c2e..c649bb1f 100644 Binary files a/artifacts/test_program_methods/pda_fund_spend_proxy.bin and b/artifacts/test_program_methods/pda_fund_spend_proxy.bin differ diff --git a/artifacts/test_program_methods/pda_spend_proxy.bin b/artifacts/test_program_methods/pda_spend_proxy.bin new file mode 100644 index 00000000..9f0d0c3e Binary files /dev/null and b/artifacts/test_program_methods/pda_spend_proxy.bin differ diff --git a/artifacts/test_program_methods/pinata_cooldown.bin b/artifacts/test_program_methods/pinata_cooldown.bin index 8b92c83c..a2194ba1 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 db60530e..9fcb05cf 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 40a5087b..92749650 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 949f4ac7..78a75df9 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 2f87b681..296863e9 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 2e7cb900..34c8f27c 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 0a0371fe..ad2df7c1 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 a711fb3d..4eef9a03 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/examples/program_deployment/README.md b/examples/program_deployment/README.md index 61e530e3..11952c35 100644 --- a/examples/program_deployment/README.md +++ b/examples/program_deployment/README.md @@ -332,7 +332,7 @@ Unlike the public version, `run_hello_world_private.rs` must: Luckily all that complexity is hidden behind the `wallet_core.send_privacy_preserving_tx` function: ```rust - let accounts = vec![PrivacyPreservingAccount::PrivateOwned(account_id)]; + let accounts = vec![AccountIdentity::PrivateOwned(account_id)]; // Construct and submit the privacy-preserving transaction wallet_core diff --git a/examples/program_deployment/src/bin/run_hello_world_private.rs b/examples/program_deployment/src/bin/run_hello_world_private.rs index 11a6c0f6..725019f1 100644 --- a/examples/program_deployment/src/bin/run_hello_world_private.rs +++ b/examples/program_deployment/src/bin/run_hello_world_private.rs @@ -1,5 +1,5 @@ use nssa::{AccountId, program::Program}; -use wallet::{PrivacyPreservingAccount, WalletCore}; +use wallet::{AccountIdentity, WalletCore}; // Before running this example, compile the `hello_world.rs` guest program with: // @@ -44,7 +44,7 @@ async fn main() { // Define the desired greeting in ASCII let greeting: Vec = vec![72, 111, 108, 97, 32, 109, 117, 110, 100, 111, 33]; - let accounts = vec![PrivacyPreservingAccount::PrivateOwned(account_id)]; + let accounts = vec![AccountIdentity::PrivateOwned(account_id)]; // Construct and submit the privacy-preserving transaction wallet_core @@ -52,7 +52,6 @@ async fn main() { accounts, Program::serialize_instruction(greeting).unwrap(), &program.into(), - None, ) .await .unwrap(); diff --git a/examples/program_deployment/src/bin/run_hello_world_through_tail_call_private.rs b/examples/program_deployment/src/bin/run_hello_world_through_tail_call_private.rs index f01c83e7..d68e99dc 100644 --- a/examples/program_deployment/src/bin/run_hello_world_through_tail_call_private.rs +++ b/examples/program_deployment/src/bin/run_hello_world_through_tail_call_private.rs @@ -4,7 +4,7 @@ use nssa::{ AccountId, ProgramId, privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program, }; -use wallet::{PrivacyPreservingAccount, WalletCore}; +use wallet::{AccountIdentity, WalletCore}; // Before running this example, compile the `simple_tail_call.rs` guest program with: // @@ -51,7 +51,7 @@ async fn main() { std::iter::once((hello_world.id(), hello_world)).collect(); let program_with_dependencies = ProgramWithDependencies::new(simple_tail_call, dependencies); - let accounts = vec![PrivacyPreservingAccount::PrivateOwned(account_id)]; + let accounts = vec![AccountIdentity::PrivateOwned(account_id)]; // Construct and submit the privacy-preserving transaction let instruction = (); @@ -60,7 +60,6 @@ async fn main() { accounts, Program::serialize_instruction(instruction).unwrap(), &program_with_dependencies, - None, ) .await .unwrap(); diff --git a/examples/program_deployment/src/bin/run_hello_world_with_move_function.rs b/examples/program_deployment/src/bin/run_hello_world_with_move_function.rs index 7e5fa5d8..e6f667a6 100644 --- a/examples/program_deployment/src/bin/run_hello_world_with_move_function.rs +++ b/examples/program_deployment/src/bin/run_hello_world_with_move_function.rs @@ -2,7 +2,7 @@ use clap::{Parser, Subcommand}; use common::transaction::NSSATransaction; use nssa::{PublicTransaction, program::Program, public_transaction}; use sequencer_service_rpc::RpcClient as _; -use wallet::{PrivacyPreservingAccount, WalletCore}; +use wallet::{AccountIdentity, WalletCore}; // Before running this example, compile the `hello_world_with_move_function.rs` guest program with: // @@ -99,14 +99,13 @@ async fn main() { } => { let instruction: Instruction = (WRITE_FUNCTION_ID, greeting.into_bytes()); let account_id = account_id.parse().unwrap(); - let accounts = vec![PrivacyPreservingAccount::PrivateOwned(account_id)]; + let accounts = vec![AccountIdentity::PrivateOwned(account_id)]; wallet_core .send_privacy_preserving_tx( accounts, Program::serialize_instruction(instruction).unwrap(), &program.into(), - None, ) .await .unwrap(); @@ -139,8 +138,8 @@ async fn main() { let to = to.parse().unwrap(); let accounts = vec![ - PrivacyPreservingAccount::Public(from), - PrivacyPreservingAccount::PrivateOwned(to), + AccountIdentity::Public(from), + AccountIdentity::PrivateOwned(to), ]; wallet_core @@ -148,7 +147,6 @@ async fn main() { accounts, Program::serialize_instruction(instruction).unwrap(), &program.into(), - None, ) .await .unwrap(); diff --git a/integration_tests/tests/auth_transfer/private.rs b/integration_tests/tests/auth_transfer/private.rs index feb5e5e8..a77ccf34 100644 --- a/integration_tests/tests/auth_transfer/private.rs +++ b/integration_tests/tests/auth_transfer/private.rs @@ -704,6 +704,7 @@ async fn ppt_that_chain_calls_faucet_is_dropped() -> Result<()> { npk, ssk, identifier: 1337, + seed: None, }, ], &program_with_deps, diff --git a/integration_tests/tests/private_pda.rs b/integration_tests/tests/private_pda.rs index d1e79ab3..b0e3336e 100644 --- a/integration_tests/tests/private_pda.rs +++ b/integration_tests/tests/private_pda.rs @@ -6,27 +6,37 @@ use std::{path::PathBuf, time::Duration}; use anyhow::{Context as _, Result}; +use authenticated_transfer_core::Instruction as AuthTransferInstruction; +use common::transaction::NSSATransaction; use integration_tests::{ - NSSA_PROGRAM_FOR_TEST_PDA_FUND_SPEND_PROXY, TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, + NSSA_PROGRAM_FOR_TEST_PDA_SPEND_PROXY, TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, verify_commitment_is_in_state, }; +use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder; use log::info; use nssa::{ - AccountId, ProgramId, privacy_preserving_transaction::circuit::ProgramWithDependencies, + AccountId, PrivacyPreservingTransaction, ProgramId, + privacy_preserving_transaction::{ + circuit::{ProgramWithDependencies, execute_and_prove}, + message::Message, + witness_set::WitnessSet, + }, program::Program, }; -use nssa_core::{NullifierPublicKey, encryption::ViewingPublicKey, program::PdaSeed}; +use nssa_core::{ + InputAccountIdentity, NullifierPublicKey, + account::{Account, AccountWithMetadata}, + encryption::ViewingPublicKey, + program::PdaSeed, +}; +use sequencer_service_rpc::RpcClient as _; use tokio::test; use wallet::{ - PrivacyPreservingAccount, WalletCore, + AccountIdentity, WalletCore, cli::{Command, account::AccountSubcommand}, }; -/// Funds a private PDA via the proxy program with a chained call to `auth_transfer`. -/// -/// A direct call to `auth_transfer` cannot establish the PDA-to-npk binding because it uses -/// `Claim::Authorized` rather than `Claim::Pda`. Routing through the proxy provides the binding -/// via `pda_seeds` in the chained call to `auth_transfer`. +/// Funds a private PDA by calling `auth_transfer` directly. #[expect( clippy::too_many_arguments, reason = "test helper — grouping args would obscure intent" @@ -34,33 +44,68 @@ use wallet::{ async fn fund_private_pda( wallet: &WalletCore, sender: AccountId, - pda_account_id: AccountId, npk: NullifierPublicKey, vpk: ViewingPublicKey, identifier: u128, seed: PdaSeed, + authority_program_id: ProgramId, amount: u128, - proxy_program: &ProgramWithDependencies, - auth_transfer_id: ProgramId, + auth_transfer: &ProgramWithDependencies, ) -> Result<()> { - wallet - .send_privacy_preserving_tx( - vec![ - PrivacyPreservingAccount::Public(sender), - PrivacyPreservingAccount::PrivatePdaForeign { - account_id: pda_account_id, - npk, - vpk, - identifier, - }, - ], - Program::serialize_instruction((seed, amount, auth_transfer_id, true)) - .context("failed to serialize pda_fund_spend_proxy fund instruction")?, - proxy_program, - None, - ) + let pda_account_id = AccountId::for_private_pda(&authority_program_id, &seed, &npk, identifier); + let sender_account = wallet + .get_account_public(sender) .await - .map_err(|e| anyhow::anyhow!("{e}"))?; + .map_err(|e| anyhow::anyhow!("failed to get sender account: {e}"))?; + let sender_sk = wallet + .get_account_public_signing_key(sender) + .context("sender signing key not found")?; + + let sender_pre = AccountWithMetadata::new(sender_account.clone(), true, sender); + let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_account_id); + + let eph_holder = EphemeralKeyHolder::new(&npk); + let ssk = eph_holder.calculate_shared_secret_sender(&vpk); + let epk = eph_holder.generate_ephemeral_public_key(); + + let instruction = Program::serialize_instruction(AuthTransferInstruction::Transfer { amount }) + .context("failed to serialize auth_transfer instruction")?; + + let account_identities = vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivatePdaInit { + npk, + ssk, + identifier, + seed: Some((seed, authority_program_id)), + }, + ]; + + let (output, proof) = execute_and_prove( + vec![sender_pre, pda_pre], + instruction, + account_identities, + auth_transfer, + ) + .map_err(|e| anyhow::anyhow!("circuit proving failed: {e}"))?; + + let message = Message::try_from_circuit_output( + vec![sender], + vec![sender_account.nonce], + vec![(npk, vpk, epk)], + output, + ) + .map_err(|e| anyhow::anyhow!("message build failed: {e}"))?; + + let witness_set = WitnessSet::for_message(&message, proof, &[sender_sk]); + let tx = PrivacyPreservingTransaction::new(message, witness_set); + + wallet + .sequencer_client + .send_transaction(NSSATransaction::PrivacyPreserving(tx)) + .await + .map_err(|e| anyhow::anyhow!("send transaction failed: {e}"))?; + Ok(()) } @@ -79,22 +124,21 @@ async fn spend_private_pda( seed: PdaSeed, amount: u128, spend_program: &ProgramWithDependencies, - auth_transfer_id: nssa::ProgramId, + auth_transfer_id: ProgramId, ) -> Result<()> { wallet .send_privacy_preserving_tx( vec![ - PrivacyPreservingAccount::PrivatePdaOwned(pda_account_id), - PrivacyPreservingAccount::PrivateForeign { + AccountIdentity::PrivatePdaOwned(pda_account_id), + AccountIdentity::PrivateForeign { npk: recipient_npk, vpk: recipient_vpk, identifier: 0, }, ], - Program::serialize_instruction((seed, amount, auth_transfer_id, false)) - .context("failed to serialize pda_fund_spend_proxy instruction")?, + Program::serialize_instruction((seed, amount, auth_transfer_id)) + .context("failed to serialize pda_spend_proxy instruction")?, spend_program, - None, ) .await .map_err(|e| anyhow::anyhow!("{e}"))?; @@ -126,9 +170,9 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> { let proxy = { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../artifacts/test_program_methods") - .join(NSSA_PROGRAM_FOR_TEST_PDA_FUND_SPEND_PROXY); + .join(NSSA_PROGRAM_FOR_TEST_PDA_SPEND_PROXY); Program::new(std::fs::read(&path).with_context(|| format!("reading {path:?}"))?) - .context("invalid pda_fund_spend_proxy binary")? + .context("invalid pda_spend_proxy binary")? }; let auth_transfer = Program::authenticated_transfer_program(); let proxy_id = proxy.id(); @@ -136,6 +180,7 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> { let seed = PdaSeed::new([42; 32]); let amount: u128 = 100; + let auth_transfer_program = ProgramWithDependencies::new(auth_transfer.clone(), [].into()); let spend_program = ProgramWithDependencies::new(proxy, [(auth_transfer_id, auth_transfer)].into()); @@ -153,14 +198,13 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> { fund_private_pda( ctx.wallet(), sender_0, - alice_pda_0_id, alice_npk, alice_vpk.clone(), 0, seed, + proxy_id, amount, - &spend_program, - auth_transfer_id, + &auth_transfer_program, ) .await?; @@ -168,14 +212,13 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> { fund_private_pda( ctx.wallet(), sender_1, - alice_pda_1_id, alice_npk, alice_vpk.clone(), 1, seed, + proxy_id, amount, - &spend_program, - auth_transfer_id, + &auth_transfer_program, ) .await?; diff --git a/nssa/core/src/circuit_io.rs b/nssa/core/src/circuit_io.rs index 63c188ef..b1c2e44f 100644 --- a/nssa/core/src/circuit_io.rs +++ b/nssa/core/src/circuit_io.rs @@ -5,7 +5,7 @@ use crate::{ NullifierSecretKey, SharedSecretKey, account::{Account, AccountWithMetadata}, encryption::Ciphertext, - program::{BlockValidityWindow, ProgramId, ProgramOutput, TimestampValidityWindow}, + program::{BlockValidityWindow, PdaSeed, ProgramId, ProgramOutput, TimestampValidityWindow}, }; #[derive(Serialize, Deserialize)] @@ -60,15 +60,28 @@ pub enum InputAccountIdentity { npk: NullifierPublicKey, ssk: SharedSecretKey, identifier: Identifier, + /// When `Some((seed, authority_program_id))`, the circuit binds this position via the + /// external derivation check + /// `AccountId::for_private_pda(authority_program_id, seed, npk, identifier) == + /// pre_state.account_id` rather than requiring a `Claim::Pda` or caller + /// `pda_seeds` to establish the binding. The `pre_state` must have `is_authorized + /// == false`. + seed: Option<(PdaSeed, ProgramId)>, }, - /// Update of an existing private PDA, authorized, with membership proof. `npk` is derived - /// from `nsk`. Authorization is established upstream by a caller `pda_seeds` match or a + /// Update of an existing private PDA, with membership proof. `npk` is derived + /// from `nsk`. Authorization may be established upstream by a caller `pda_seeds` match or a /// previously-seen authorization in a chained call. PrivatePdaUpdate { ssk: SharedSecretKey, nsk: NullifierSecretKey, membership_proof: MembershipProof, identifier: Identifier, + /// When `Some((seed, authority_program_id))`, the circuit binds this position via the + /// external derivation check + /// `AccountId::for_private_pda(authority_program_id, seed, npk, identifier) == + /// pre_state.account_id` rather than requiring a caller `pda_seeds` to establish + /// the binding. The `pre_state` must have `is_authorized == false`. + seed: Option<(PdaSeed, ProgramId)>, }, } diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index 915c8d3e..902f5eaa 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -461,6 +461,7 @@ mod tests { npk, ssk: shared_secret, identifier, + seed: None, }], &program.clone().into(), ) @@ -488,7 +489,7 @@ mod tests { let seed = PdaSeed::new([42; 32]); let shared_secret_pda = SharedSecretKey::new([55; 32], &keys.vpk()); - // PDA (new, mask 3) + // PDA (new, private PDA) let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 0); let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id); @@ -506,6 +507,7 @@ mod tests { npk, ssk: shared_secret_pda, identifier: 0, + seed: None, }], &program_with_deps, ); @@ -557,6 +559,7 @@ mod tests { npk, ssk: shared_secret_pda, identifier: 0, + seed: None, }, InputAccountIdentity::Public, ], @@ -747,7 +750,7 @@ mod tests { /// to `PrivateAccountKind::Pda` carrying the correct `(program_id, seed, identifier)`. #[test] fn private_pda_update_encrypts_pda_kind_with_identifier() { - let program = Program::pda_fund_spend_proxy(); + let program = Program::pda_spend_proxy(); let auth_transfer = Program::authenticated_transfer_program(); let keys = test_private_account_keys_1(); let npk = keys.npk(); @@ -784,6 +787,7 @@ mod tests { nsk: keys.nsk, membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(), identifier, + seed: None, }, InputAccountIdentity::Public, ], @@ -819,6 +823,7 @@ mod tests { npk, ssk: shared_secret, identifier: 99, + seed: None, }], &program.into(), ); @@ -828,7 +833,7 @@ mod tests { #[test] fn private_pda_update_identifier_mismatch_fails() { - let program = Program::pda_fund_spend_proxy(); + let program = Program::pda_spend_proxy(); let auth_transfer = Program::authenticated_transfer_program(); let keys = test_private_account_keys_1(); let npk = keys.npk(); @@ -862,6 +867,7 @@ mod tests { nsk: keys.nsk, membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(), identifier: 99, + seed: None, }, InputAccountIdentity::Public, ], diff --git a/nssa/src/program.rs b/nssa/src/program.rs index c3c92f1e..696c2086 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -350,12 +350,12 @@ mod tests { } #[must_use] - pub fn pda_fund_spend_proxy() -> Self { - use test_program_methods::{PDA_FUND_SPEND_PROXY_ELF, PDA_FUND_SPEND_PROXY_ID}; + pub fn pda_spend_proxy() -> Self { + use test_program_methods::{PDA_SPEND_PROXY_ELF, PDA_SPEND_PROXY_ID}; Self { - id: PDA_FUND_SPEND_PROXY_ID, - elf: PDA_FUND_SPEND_PROXY_ELF.to_vec(), + id: PDA_SPEND_PROXY_ID, + elf: PDA_SPEND_PROXY_ELF.to_vec(), } } diff --git a/nssa/src/state.rs b/nssa/src/state.rs index e9f2058f..0f38f9f3 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -2218,7 +2218,7 @@ pub mod tests { assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); } - /// A mask-3 account that no program claims via `Claim::Pda` and no caller authorizes via + /// A private PDA account that no program claims via `Claim::Pda` and no caller authorizes via /// `ChainedCall.pda_seeds` has no binding between its supplied npk and its `account_id`, /// so the circuit must reject. Here `simple_balance_transfer` emits no claim for the /// second account, leaving position 1 unbound. @@ -2249,6 +2249,7 @@ pub mod tests { npk, ssk: shared_secret, identifier: u128::MAX, + seed: None, }, ], &program.into(), @@ -2257,7 +2258,7 @@ pub mod tests { assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); } - /// Happy path: a program claims a new mask-3 account via `Claim::Pda(seed)`. The circuit + /// Happy path: a program claims a new private PDA via `Claim::Pda(seed)`. The circuit /// reads the npk for that `pre_state` from `private_account_keys` at the `pre_state`'s /// position, derives `AccountId` via `AccountId::for_private_pda(program_id, seed, npk)`, and /// asserts it equals the `pre_state`'s `account_id`. The equality both validates the claim @@ -2280,11 +2281,12 @@ pub mod tests { npk, ssk: shared_secret, identifier: u128::MAX, + seed: None, }], &program.into(), ); - let (output, _proof) = result.expect("mask-3 private PDA claim should succeed"); + let (output, _proof) = result.expect("private PDA claim should succeed"); assert_eq!(output.new_nullifiers.len(), 1); assert_eq!(output.new_commitments.len(), 1); assert_eq!(output.ciphertexts.len(), 1); @@ -2319,6 +2321,7 @@ pub mod tests { npk: npk_b, ssk: shared_secret, identifier: u128::MAX, + seed: None, }], &program.into(), ); @@ -2326,7 +2329,7 @@ pub mod tests { assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); } - /// Happy path for the caller-seeds authorization of a mask-3 PDA. The delegator claims a + /// Happy path for the caller-seeds authorization of a private PDA. The delegator claims a /// private PDA via `Claim::Pda(seed)`, then chains to a callee (`noop`) delegating the same /// seed via `ChainedCall.pda_seeds`. In the callee's step, the `pre_state`'s authorization /// is established via the private derivation @@ -2354,12 +2357,13 @@ pub mod tests { npk, ssk: shared_secret, identifier: u128::MAX, + seed: None, }], &program_with_deps, ); let (output, _proof) = - result.expect("caller-seeds authorization of mask-3 private PDA should succeed"); + result.expect("caller-seeds authorization of private PDA should succeed"); assert_eq!(output.new_commitments.len(), 1); assert_eq!(output.new_nullifiers.len(), 1); } @@ -2392,6 +2396,7 @@ pub mod tests { npk, ssk: shared_secret, identifier: u128::MAX, + seed: None, }], &program_with_deps, ); @@ -2401,8 +2406,8 @@ pub mod tests { /// Exploit-scenario pin. A single `(program_id, seed)` pair can derive a family of /// `AccountId`s, one public PDA and one private PDA per distinct npk. Without the tx-wide - /// family-binding check, a program could claim `PDA_alice` (mask-3, `alice_npk`) and - /// `PDA_bob` (mask-3, `bob_npk`) under the same seed in one transaction, and once reuse + /// family-binding check, a program could claim `PDA_alice` (`alice_npk`) and + /// `PDA_bob` (`bob_npk`) under the same seed in one transaction, and once reuse /// is supported a later chained call could delegate both to a callee via /// `pda_seeds: [S]` and mix balances across them. The binding check rejects the setup /// here: after the first claim records `(program, seed) → PDA_alice`, the second claim @@ -2430,11 +2435,13 @@ pub mod tests { npk: keys_a.npk(), ssk: shared_a, identifier: u128::MAX, + seed: None, }, InputAccountIdentity::PrivatePdaInit { npk: keys_b.npk(), ssk: shared_b, identifier: u128::MAX, + seed: None, }, ], &program.into(), @@ -2443,17 +2450,11 @@ pub mod tests { assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); } - /// Pins the current limitation: a mask-3 PDA that was claimed in a previous transaction - /// cannot be re-used in a new transaction as-is. This PR only binds supplied npks via a - /// fresh `Claim::Pda` or a caller's `ChainedCall.pda_seeds`, neither is present when a - /// program operates on an already-owned private PDA at top level. The reject site is the - /// post-loop `private_pda_bound_positions` assertion in - /// `privacy_preserving_circuit.rs`: `noop` emits no `Claim::Pda` and there is no caller + /// A private PDA that is reused at top level without an external seed in the identity still + /// fails binding. The noop program emits no `Claim::Pda` and there is no caller /// `ChainedCall.pda_seeds`, so position 0 is never bound and the assertion fires. - // TODO: a follow-up PR in the Private PDAs series needs to let the wallet supply a - // `(seed, original_owner_program_id)` side input per mask-3 `pre_state` so the circuit - // can re-verify `AccountId::for_private_pda(owner, seed, npk) == pre.account_id` without a - // claim. + /// Supplying `seed: Some((seed, owner_program_id))` in the `PrivatePdaUpdate` identity is + /// the correct path for top-level reuse; this test pins the failure when no seed is provided. #[test] fn private_pda_top_level_reuse_rejected_by_binding_check() { let program = Program::noop(); @@ -2481,6 +2482,7 @@ pub mod tests { npk, ssk: shared_secret, identifier: u128::MAX, + seed: None, }], &program.into(), ); @@ -4372,15 +4374,15 @@ pub mod tests { let alice_keys = test_private_account_keys_1(); let alice_npk = alice_keys.npk(); - let proxy = Program::pda_fund_spend_proxy(); + let proxy = Program::pda_spend_proxy(); let auth_transfer = Program::authenticated_transfer_program(); let proxy_id = proxy.id(); let auth_transfer_id = auth_transfer.id(); let seed = PdaSeed::new([42; 32]); let amount: u128 = 100; - let program_with_deps = - ProgramWithDependencies::new(proxy, [(auth_transfer_id, auth_transfer)].into()); + let spend_with_deps = + ProgramWithDependencies::new(proxy, [(auth_transfer_id, auth_transfer.clone())].into()); let funder_id = funder_keys.account_id(); let alice_pda_0_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, 0); @@ -4406,7 +4408,7 @@ pub mod tests { let alice_shared_0 = SharedSecretKey::new([10; 32], &alice_keys.vpk()); let alice_shared_1 = SharedSecretKey::new([11; 32], &alice_keys.vpk()); - // Fund alice_pda_0 + // Fund alice_pda_0 via authenticated_transfer directly. { let funder_account = state.get_account_by_id(funder_id); let funder_nonce = funder_account.nonce; @@ -4415,16 +4417,18 @@ pub mod tests { AccountWithMetadata::new(funder_account, true, funder_id), AccountWithMetadata::new(Account::default(), false, alice_pda_0_id), ], - Program::serialize_instruction((seed, amount, auth_transfer_id, true)).unwrap(), + Program::serialize_instruction(AuthTransferInstruction::Transfer { amount }) + .unwrap(), vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivatePdaInit { npk: alice_npk, ssk: alice_shared_0, identifier: 0, + seed: Some((seed, proxy_id)), }, ], - &program_with_deps, + &auth_transfer.clone().into(), ) .unwrap(); let message = Message::try_from_circuit_output( @@ -4448,7 +4452,7 @@ pub mod tests { .unwrap(); } - // Fund alice_pda_1 + // Fund alice_pda_1 the same way with identifier 1. { let funder_account = state.get_account_by_id(funder_id); let funder_nonce = funder_account.nonce; @@ -4457,16 +4461,18 @@ pub mod tests { AccountWithMetadata::new(funder_account, true, funder_id), AccountWithMetadata::new(Account::default(), false, alice_pda_1_id), ], - Program::serialize_instruction((seed, amount, auth_transfer_id, true)).unwrap(), + Program::serialize_instruction(AuthTransferInstruction::Transfer { amount }) + .unwrap(), vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivatePdaInit { npk: alice_npk, ssk: alice_shared_1, identifier: 1, + seed: Some((seed, proxy_id)), }, ], - &program_with_deps, + &auth_transfer.into(), ) .unwrap(); let message = Message::try_from_circuit_output( @@ -4504,7 +4510,7 @@ pub mod tests { AccountWithMetadata::new(alice_pda_0_account, true, alice_pda_0_id), AccountWithMetadata::new(recipient_account, true, recipient_id), ], - Program::serialize_instruction((seed, amount, auth_transfer_id, false)).unwrap(), + Program::serialize_instruction((seed, amount, auth_transfer_id)).unwrap(), vec![ InputAccountIdentity::PrivatePdaUpdate { ssk: alice_shared_0, @@ -4513,10 +4519,11 @@ pub mod tests { .get_proof_for_commitment(&commitment_pda_0) .expect("pda_0 must be in state"), identifier: 0, + seed: None, }, InputAccountIdentity::Public, ], - &program_with_deps, + &spend_with_deps, ) .unwrap(); let message = Message::try_from_circuit_output( @@ -4545,10 +4552,10 @@ pub mod tests { let recipient_account = state.get_account_by_id(recipient_id); let (output, proof) = execute_and_prove( vec![ - AccountWithMetadata::new(alice_pda_1_account, true, alice_pda_1_id), + AccountWithMetadata::new(alice_pda_1_account.clone(), true, alice_pda_1_id), AccountWithMetadata::new(recipient_account, false, recipient_id), ], - Program::serialize_instruction((seed, amount, auth_transfer_id, false)).unwrap(), + Program::serialize_instruction((seed, amount, auth_transfer_id)).unwrap(), vec![ InputAccountIdentity::PrivatePdaUpdate { ssk: alice_shared_1, @@ -4557,10 +4564,11 @@ pub mod tests { .get_proof_for_commitment(&commitment_pda_1) .expect("pda_1 must be in state"), identifier: 1, + seed: None, }, InputAccountIdentity::Public, ], - &program_with_deps, + &spend_with_deps, ) .unwrap(); let message = Message::try_from_circuit_output( @@ -4585,5 +4593,70 @@ pub mod tests { } assert_eq!(state.get_account_by_id(recipient_id).balance, 2 * amount); + + // Re-fund alice_pda_1 top-level via auth_transfer using PrivatePdaUpdate with an + // external seed. + let alice_pda_1_account_after_spend = Account { + program_owner: auth_transfer_id, + balance: 0, + nonce: alice_pda_1_account + .nonce + .private_account_nonce_increment(&alice_keys.nsk), + ..Account::default() + }; + let commitment_pda_1_after_spend = + Commitment::new(&alice_pda_1_id, &alice_pda_1_account_after_spend); + let alice_shared_1_refund = SharedSecretKey::new([12; 32], &alice_keys.vpk()); + { + let recipient_account = state.get_account_by_id(recipient_id); + let recipient_nonce = recipient_account.nonce; + let (output, proof) = execute_and_prove( + vec![ + AccountWithMetadata::new(recipient_account, true, recipient_id), + AccountWithMetadata::new( + alice_pda_1_account_after_spend, + false, + alice_pda_1_id, + ), + ], + Program::serialize_instruction(AuthTransferInstruction::Transfer { amount }) + .unwrap(), + vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivatePdaUpdate { + nsk: alice_keys.nsk, + ssk: alice_shared_1_refund, + membership_proof: state + .get_proof_for_commitment(&commitment_pda_1_after_spend) + .expect("pda_1 after spend must be in state"), + identifier: 1, + seed: Some((seed, proxy_id)), + }, + ], + &Program::authenticated_transfer_program().into(), + ) + .unwrap(); + let message = Message::try_from_circuit_output( + vec![recipient_id], + vec![recipient_nonce], + vec![( + alice_npk, + alice_keys.vpk(), + EphemeralPublicKey::from_scalar([12; 32]), + )], + output, + ) + .unwrap(); + let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_signing_key]); + state + .transition_from_privacy_preserving_transaction( + &PrivacyPreservingTransaction::new(message, witness_set), + 5, + 0, + ) + .unwrap(); + } + + assert_eq!(state.get_account_by_id(recipient_id).balance, amount); } } diff --git a/program_methods/guest/src/bin/privacy_preserving_circuit/execution_state.rs b/program_methods/guest/src/bin/privacy_preserving_circuit/execution_state.rs index c65b1d29..c06698d6 100644 --- a/program_methods/guest/src/bin/privacy_preserving_circuit/execution_state.rs +++ b/program_methods/guest/src/bin/privacy_preserving_circuit/execution_state.rs @@ -305,6 +305,68 @@ impl ExecutionState { } Entry::Vacant(_) => { // Pre state for the initial call + let pre_state_position = self.pre_states.len(); + let external_seed = match account_identities.get(pre_state_position) { + Some(InputAccountIdentity::PrivatePdaInit { + npk, + identifier, + seed: Some((seed, authority_program_id)), + .. + }) => { + let expected = AccountId::for_private_pda( + authority_program_id, + seed, + npk, + *identifier, + ); + assert_eq!( + pre_account_id, expected, + "External seed mismatch for PrivatePdaInit at position {pre_state_position}" + ); + Some((*seed, *authority_program_id)) + } + Some(InputAccountIdentity::PrivatePdaUpdate { + nsk, + identifier, + seed: Some((seed, authority_program_id)), + .. + }) => { + let npk = NullifierPublicKey::from(nsk); + let expected = AccountId::for_private_pda( + authority_program_id, + seed, + &npk, + *identifier, + ); + assert_eq!( + pre_account_id, expected, + "External seed mismatch for PrivatePdaUpdate at position {pre_state_position}" + ); + Some((*seed, *authority_program_id)) + } + _ => None, + }; + // External seed is only consulted the first time the account is seen. + // Subsequent calls need no re-check because the entry is already recorded on + // private_pda_bound_positions. + if let Some((seed, authority_program_id)) = external_seed { + assert!( + !pre.is_authorized, + "Private PDA with externally-provided seed must not be authorized at position {pre_state_position}" + ); + bind_private_pda_position( + &mut self.private_pda_bound_positions, + pre_state_position, + authority_program_id, + seed, + ); + assert_family_binding( + &mut self.pda_family_binding, + authority_program_id, + seed, + pre_account_id, + ); + } self.pre_states.push(pre); } } @@ -348,14 +410,11 @@ impl ExecutionState { ); } } - } else if account_identity.is_private_pda() { + } else { + // Private accounts: don't enforce the claim semantics. Unauthorized private + // claiming is intentionally allowed match claim { - Claim::Authorized => { - assert!( - pre_is_authorized, - "Cannot claim unauthorized private PDA {pre_account_id}" - ); - } + Claim::Authorized => {} Claim::Pda(seed) => { let (npk, identifier) = self .private_pda_npk_by_position @@ -383,10 +442,6 @@ impl ExecutionState { ); } } - } else { - // Standalone private accounts: don't enforce the claim semantics. - // Unauthorized private claiming is intentionally allowed since operating - // these accounts requires the npk/nsk keypair anyway. } post.account_mut().program_owner = program_id; diff --git a/program_methods/guest/src/bin/privacy_preserving_circuit/output.rs b/program_methods/guest/src/bin/privacy_preserving_circuit/output.rs index f5a6d1f9..6e302401 100644 --- a/program_methods/guest/src/bin/privacy_preserving_circuit/output.rs +++ b/program_methods/guest/src/bin/privacy_preserving_circuit/output.rs @@ -148,6 +148,7 @@ pub fn compute_circuit_output( npk: _, ssk, identifier, + seed: _, } => { // The npk-to-account_id binding is established upstream in // `validate_and_sync_states` via `Claim::Pda(seed)` or a caller `pda_seeds` @@ -172,7 +173,7 @@ pub fn compute_circuit_output( let new_nonce = Nonce::private_account_nonce_init(&pre_state.account_id); let account_id = pre_state.account_id; - let (pda_program_id, seed) = pda_seed_by_position + let (authority_program_id, seed) = pda_seed_by_position .get(&pos) .expect("PrivatePdaInit position must be in pda_seed_by_position"); emit_private_output( @@ -181,7 +182,7 @@ pub fn compute_circuit_output( post_state, &account_id, &PrivateAccountKind::Pda { - program_id: *pda_program_id, + program_id: *authority_program_id, seed: *seed, identifier: *identifier, }, @@ -195,14 +196,16 @@ pub fn compute_circuit_output( nsk, membership_proof, identifier, + seed: external_seed, } => { - // The npk binding is established upstream. Authorization must already be set; - // an unauthorized PrivatePdaUpdate would mean the prover supplied an nsk for an - // unbound PDA, which the upstream binding check would have rejected anyway, - // but we assert here to fail fast and document the precondition. + // With an external seed the binding comes from the circuit input and the + // pre_state is intentionally unauthorized; without one the binding comes from + // a Claim or caller pda_seeds, so the pre_state must already be authorized. + // When `external_seed` is `Some`, execution_state already asserted + // `!pre_state.is_authorized`. assert!( - pre_state.is_authorized, - "PrivatePdaUpdate requires authorized pre_state" + pre_state.is_authorized ^ external_seed.is_some(), + "PrivatePdaUpdate requires authorized pre_state or external seed" ); let new_nullifier = compute_update_nullifier_and_set_digest( @@ -214,7 +217,7 @@ pub fn compute_circuit_output( let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk); let account_id = pre_state.account_id; - let (pda_program_id, seed) = pda_seed_by_position + let (authority_program_id, seed) = pda_seed_by_position .get(&pos) .expect("PrivatePdaUpdate position must be in pda_seed_by_position"); emit_private_output( @@ -223,7 +226,7 @@ pub fn compute_circuit_output( post_state, &account_id, &PrivateAccountKind::Pda { - program_id: *pda_program_id, + program_id: *authority_program_id, seed: *seed, identifier: *identifier, }, diff --git a/test_fixtures/src/lib.rs b/test_fixtures/src/lib.rs index cee4d5f8..b22b6fe7 100644 --- a/test_fixtures/src/lib.rs +++ b/test_fixtures/src/lib.rs @@ -34,7 +34,7 @@ pub mod setup; pub const TIME_TO_WAIT_FOR_BLOCK_SECONDS: u64 = 12; pub const NSSA_PROGRAM_FOR_TEST_DATA_CHANGER: &str = "data_changer.bin"; pub const NSSA_PROGRAM_FOR_TEST_NOOP: &str = "noop.bin"; -pub const NSSA_PROGRAM_FOR_TEST_PDA_FUND_SPEND_PROXY: &str = "pda_fund_spend_proxy.bin"; +pub const NSSA_PROGRAM_FOR_TEST_PDA_SPEND_PROXY: &str = "pda_spend_proxy.bin"; pub(crate) const BEDROCK_SERVICE_WITH_OPEN_PORT: &str = "logos-blockchain-node-0"; pub(crate) const BEDROCK_SERVICE_PORT: u16 = 18080; diff --git a/test_fixtures/src/setup.rs b/test_fixtures/src/setup.rs index 54bdc493..5d7377b1 100644 --- a/test_fixtures/src/setup.rs +++ b/test_fixtures/src/setup.rs @@ -9,9 +9,7 @@ use sequencer_service::{GenesisAction, SequencerHandle}; use sequencer_service_rpc::RpcClient as _; use tempfile::TempDir; use testcontainers::compose::DockerCompose; -use wallet::{ - AccDecodeData::Decode, PrivacyPreservingAccount, WalletCore, config::WalletConfigOverrides, -}; +use wallet::{AccDecodeData::Decode, AccountIdentity, WalletCore, config::WalletConfigOverrides}; use crate::{ BEDROCK_SERVICE_PORT, BEDROCK_SERVICE_WITH_OPEN_PORT, @@ -293,12 +291,11 @@ async fn claim_funds_from_vault_to_private( let (tx_hash, mut secrets) = wallet .send_privacy_preserving_tx( vec![ - PrivacyPreservingAccount::PrivateOwned(owner_id), - PrivacyPreservingAccount::Public(owner_vault_id), + AccountIdentity::PrivateOwned(owner_id), + AccountIdentity::Public(owner_vault_id), ], instruction_data, &program_with_dependencies, - None, ) .await .context("Failed to submit private vault claim transaction")?; diff --git a/test_program_methods/guest/src/bin/pda_fund_spend_proxy.rs b/test_program_methods/guest/src/bin/pda_fund_spend_proxy.rs deleted file mode 100644 index 567f9af1..00000000 --- a/test_program_methods/guest/src/bin/pda_fund_spend_proxy.rs +++ /dev/null @@ -1,71 +0,0 @@ -use nssa_core::{ - account::AccountWithMetadata, - program::{ - AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, ProgramOutput, - read_nssa_inputs, - }, -}; -use risc0_zkvm::serde::to_vec; - -/// Proxy for interacting with private PDAs via `auth_transfer`. -/// -/// The `is_fund` flag selects the operating mode: -/// -/// - `false` (Spend): `pre_states = [pda (authorized), recipient]`. Debits the PDA. The PDA-to-npk -/// binding is established via `pda_seeds` in the chained call to `auth_transfer`. -/// -/// - `true` (Fund): `pre_states = [sender (authorized), pda (foreign/uninitialized)]`. Credits the -/// PDA. A direct call to `auth_transfer` cannot bind the PDA because `auth_transfer` uses -/// `Claim::Authorized`, not `Claim::Pda`. Routing through this proxy establishes the binding via -/// `pda_seeds` in the chained call. -type Instruction = (PdaSeed, u128, ProgramId, bool); - -fn main() { - let ( - ProgramInput { - self_program_id, - caller_program_id, - pre_states, - instruction: (seed, amount, auth_transfer_id, is_fund), - }, - instruction_words, - ) = read_nssa_inputs::(); - - let Ok([first, second]) = <[_; 2]>::try_from(pre_states) else { - return; - }; - - assert!(first.is_authorized, "first pre_state must be authorized"); - - let chained_pre_states = if is_fund { - let pda_authorized = AccountWithMetadata { - account: second.account.clone(), - account_id: second.account_id, - is_authorized: true, - }; - vec![first.clone(), pda_authorized] - } else { - vec![first.clone(), second.clone()] - }; - - let first_post = AccountPostState::new(first.account.clone()); - let second_post = AccountPostState::new(second.account.clone()); - - let chained_call = ChainedCall { - program_id: auth_transfer_id, - instruction_data: to_vec(&authenticated_transfer_core::Instruction::Transfer { amount }) - .unwrap(), - pre_states: chained_pre_states, - pda_seeds: vec![seed], - }; - - ProgramOutput::new( - self_program_id, - caller_program_id, - instruction_words, - vec![first, second], - vec![first_post, second_post], - ) - .with_chained_calls(vec![chained_call]) - .write(); -} diff --git a/test_program_methods/guest/src/bin/pda_spend_proxy.rs b/test_program_methods/guest/src/bin/pda_spend_proxy.rs new file mode 100644 index 00000000..4094e101 --- /dev/null +++ b/test_program_methods/guest/src/bin/pda_spend_proxy.rs @@ -0,0 +1,50 @@ +use nssa_core::program::{ + AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, ProgramOutput, + read_nssa_inputs, +}; +use risc0_zkvm::serde::to_vec; + +/// Proxy for spending from a private PDA via `auth_transfer`. +/// +/// `pre_states = [pda (authorized), recipient]`. Debits the PDA and credits the recipient. +/// The PDA-to-npk binding is established via `pda_seeds` in the chained call to `auth_transfer`. +type Instruction = (PdaSeed, u128, ProgramId); + +fn main() { + let ( + ProgramInput { + self_program_id, + caller_program_id, + pre_states, + instruction: (seed, amount, auth_transfer_id), + }, + instruction_words, + ) = read_nssa_inputs::(); + + let Ok([first, second]) = <[_; 2]>::try_from(pre_states) else { + return; + }; + + assert!(first.is_authorized, "first pre_state must be authorized"); + + let first_post = AccountPostState::new(first.account.clone()); + let second_post = AccountPostState::new(second.account.clone()); + + let chained_call = ChainedCall { + program_id: auth_transfer_id, + instruction_data: to_vec(&authenticated_transfer_core::Instruction::Transfer { amount }) + .unwrap(), + pre_states: vec![first.clone(), second.clone()], + pda_seeds: vec![seed], + }; + + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + vec![first, second], + vec![first_post, second_post], + ) + .with_chained_calls(vec![chained_call]) + .write(); +} diff --git a/tools/integration_bench/src/harness.rs b/tools/integration_bench/src/harness.rs index fb9d4d5c..813bbbab 100644 --- a/tools/integration_bench/src/harness.rs +++ b/tools/integration_bench/src/harness.rs @@ -15,7 +15,7 @@ use test_fixtures::{DiskSizes, TestContext}; use wallet::cli::SubcommandReturnValue; const TX_INCLUSION_POLL_INTERVAL: Duration = Duration::from_millis(250); -const TX_INCLUSION_TIMEOUT: Duration = Duration::from_secs(120); +const TX_INCLUSION_TIMEOUT: Duration = Duration::from_mins(2); /// Borsh-serialized sizes for one zone block fetched after a step. `block_bytes` /// is the full Block (header + body + bedrock metadata) and is the closest diff --git a/tools/integration_bench/src/main.rs b/tools/integration_bench/src/main.rs index ccf7058e..4d14f6d1 100644 --- a/tools/integration_bench/src/main.rs +++ b/tools/integration_bench/src/main.rs @@ -181,7 +181,7 @@ async fn measure_bedrock_finality(ctx: &TestContext) -> Result { .context("connect indexer WS")?; let sequencer_tip = ctx.sequencer_client().get_last_block_id().await?; - let timeout = Duration::from_secs(60); + let timeout = Duration::from_mins(1); let started = std::time::Instant::now(); let poll = async { loop { diff --git a/wallet/src/privacy_preserving_tx.rs b/wallet/src/account_manager.rs similarity index 70% rename from wallet/src/privacy_preserving_tx.rs rename to wallet/src/account_manager.rs index 005eaf75..c3b23c34 100644 --- a/wallet/src/privacy_preserving_tx.rs +++ b/wallet/src/account_manager.rs @@ -1,6 +1,7 @@ use anyhow::Result; use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder; -use nssa::{AccountId, PrivateKey}; +use keycard_wallet::{KeycardWallet, python_path}; +use nssa::{AccountId, PrivateKey, PublicKey, Signature}; use nssa_core::{ Identifier, InputAccountIdentity, MembershipProof, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, @@ -11,8 +12,15 @@ use nssa_core::{ use crate::{ExecutionFailureKind, WalletCore}; #[derive(Clone)] -pub enum PrivacyPreservingAccount { +pub enum AccountIdentity { Public(AccountId), + /// A public account without signing. Would not try to sign, even if account is owned. + PublicNoSign(AccountId), + /// A public account from keycard. Mandatory signing. + PublicKeycard { + account_id: AccountId, + key_path: String, + }, PrivateOwned(AccountId), PrivateForeign { npk: NullifierPublicKey, @@ -50,10 +58,15 @@ pub enum PrivacyPreservingAccount { }, } -impl PrivacyPreservingAccount { +impl AccountIdentity { #[must_use] + /// Note: `PublicNoSign` still counts as public, the variant just suppresses the signing-key + /// lookup. pub const fn is_public(&self) -> bool { - matches!(&self, Self::Public(_)) + matches!( + &self, + Self::Public(_) | Self::PublicNoSign(_) | Self::PublicKeycard { .. } + ) } #[must_use] @@ -82,23 +95,29 @@ enum State { account: AccountWithMetadata, sk: Option, }, + PublicKeycard { + account: AccountWithMetadata, + key_path: String, + }, Private(AccountPreparedData), } pub struct AccountManager { states: Vec, + pin: Option, } impl AccountManager { pub async fn new( wallet: &WalletCore, - accounts: Vec, + accounts: Vec, ) -> Result { let mut states = Vec::with_capacity(accounts.len()); + let mut pin = None; for account in accounts { let state = match account { - PrivacyPreservingAccount::Public(account_id) => { + AccountIdentity::Public(account_id) => { let acc = wallet .get_account_public(account_id) .await @@ -109,12 +128,52 @@ impl AccountManager { State::Public { account, sk } } - PrivacyPreservingAccount::PrivateOwned(account_id) => { + AccountIdentity::PublicNoSign(account_id) => { + let acc = wallet + .get_account_public(account_id) + .await + .map_err(ExecutionFailureKind::SequencerError)?; + + let sk = None; + let account = AccountWithMetadata::new(acc.clone(), sk.is_some(), account_id); + + State::Public { account, sk } + } + AccountIdentity::PublicKeycard { + account_id, + key_path, + } => { + let acc = wallet + .get_account_public(account_id) + .await + .map_err(ExecutionFailureKind::SequencerError)?; + + let account = AccountWithMetadata::new(acc.clone(), true, account_id); + + if pin.is_none() { + pin = Some( + crate::helperfunctions::read_pin() + .map_err(|e| { + ExecutionFailureKind::KeycardError(pyo3::PyErr::new::< + pyo3::exceptions::PyRuntimeError, + _, + >( + e.to_string() + )) + })? + .as_str() + .to_owned(), + ); + } + + State::PublicKeycard { account, key_path } + } + AccountIdentity::PrivateOwned(account_id) => { let pre = private_key_tree_acc_preparation(wallet, account_id, false).await?; State::Private(pre) } - PrivacyPreservingAccount::PrivateForeign { + AccountIdentity::PrivateForeign { npk, vpk, identifier, @@ -138,11 +197,11 @@ impl AccountManager { State::Private(pre) } - PrivacyPreservingAccount::PrivatePdaOwned(account_id) => { + AccountIdentity::PrivatePdaOwned(account_id) => { let pre = private_key_tree_acc_preparation(wallet, account_id, true).await?; State::Private(pre) } - PrivacyPreservingAccount::PrivatePdaForeign { + AccountIdentity::PrivatePdaForeign { account_id, npk, vpk, @@ -166,7 +225,7 @@ impl AccountManager { }; State::Private(pre) } - PrivacyPreservingAccount::PrivateShared { + AccountIdentity::PrivateShared { nsk, npk, vpk, @@ -180,7 +239,7 @@ impl AccountManager { State::Private(pre) } - PrivacyPreservingAccount::PrivatePdaShared { + AccountIdentity::PrivatePdaShared { account_id, nsk, npk, @@ -199,27 +258,33 @@ impl AccountManager { states.push(state); } - Ok(Self { states }) + Ok(Self { states, pin }) } pub fn pre_states(&self) -> Vec { self.states .iter() .map(|state| match state { - State::Public { account, .. } => account.clone(), + State::Public { account, .. } | State::PublicKeycard { account, .. } => { + account.clone() + } State::Private(pre) => pre.pre_state.clone(), }) .collect() } pub fn public_account_nonces(&self) -> Vec { - self.states - .iter() - .filter_map(|state| match state { - State::Public { account, sk } => sk.as_ref().map(|_| account.account.nonce), - State::Private(_) => None, - }) - .collect() + // Must match the signature order produced by sign_message(): local accounts first, + // keycard accounts second. + let local = self.states.iter().filter_map(|state| match state { + State::Public { account, sk } => sk.as_ref().map(|_| account.account.nonce), + State::PublicKeycard { .. } | State::Private(_) => None, + }); + let keycard = self.states.iter().filter_map(|state| match state { + State::PublicKeycard { account, .. } => Some(account.account.nonce), + State::Public { .. } | State::Private(_) => None, + }); + local.chain(keycard).collect() } pub fn private_account_keys(&self) -> Vec { @@ -232,7 +297,7 @@ impl AccountManager { vpk: pre.vpk.clone(), epk: pre.epk.clone(), }), - State::Public { .. } => None, + State::Public { .. } | State::PublicKeycard { .. } => None, }) .collect() } @@ -245,18 +310,20 @@ impl AccountManager { self.states .iter() .map(|state| match state { - State::Public { .. } => InputAccountIdentity::Public, + State::Public { .. } | State::PublicKeycard { .. } => InputAccountIdentity::Public, State::Private(pre) if pre.is_pda => match (pre.nsk, pre.proof.clone()) { (Some(nsk), Some(membership_proof)) => InputAccountIdentity::PrivatePdaUpdate { ssk: pre.ssk, nsk, membership_proof, identifier: pre.identifier, + seed: None, }, _ => InputAccountIdentity::PrivatePdaInit { npk: pre.npk, ssk: pre.ssk, identifier: pre.identifier, + seed: None, }, }, State::Private(pre) => match (pre.nsk, pre.proof.clone()) { @@ -287,21 +354,66 @@ impl AccountManager { self.states .iter() .filter_map(|state| match state { - State::Public { account, .. } => Some(account.account_id), + State::Public { account, .. } | State::PublicKeycard { account, .. } => { + Some(account.account_id) + } State::Private(_) => None, }) .collect() } - pub fn public_account_auth(&self) -> Vec<&PrivateKey> { + pub fn public_non_keycard_account_auth(&self) -> Vec<&PrivateKey> { self.states .iter() .filter_map(|state| match state { State::Public { sk, .. } => sk.as_ref(), - State::Private(_) => None, + State::PublicKeycard { .. } | State::Private(_) => None, }) .collect() } + + pub fn sign_message(&self, message_hash: [u8; 32]) -> Result> { + let mut sigs: Vec<(Signature, PublicKey)> = self + .public_non_keycard_account_auth() + .into_iter() + .map(|key| { + ( + Signature::new(key, &message_hash), + PublicKey::new_from_private_key(key), + ) + }) + .collect(); + + let keycard_paths = self + .states + .iter() + .fold(vec![], |mut acc, state| match state { + State::Private(_) | State::Public { .. } => acc, + State::PublicKeycard { + account: _, + key_path, + } => { + acc.push(key_path.as_str()); + acc + } + }); + + if let Some(pin) = self.pin.clone() { + pyo3::Python::with_gil(|py| -> pyo3::PyResult<()> { + python_path::add_python_path(py)?; + let wallet = KeycardWallet::new(py)?; + wallet.connect(py, &pin)?; + for path in keycard_paths { + sigs.push(wallet.sign_message_for_path(py, path, &message_hash)?); + } + drop(wallet.close_session(py)); + Ok(()) + }) + .map_err(anyhow::Error::from)?; + } + + Ok(sigs) + } } struct AccountPreparedData { @@ -410,7 +522,7 @@ mod tests { #[test] fn private_shared_is_private() { - let acc = PrivacyPreservingAccount::PrivateShared { + let acc = AccountIdentity::PrivateShared { nsk: [0; 32], npk: NullifierPublicKey([1; 32]), vpk: ViewingPublicKey::from_scalar([2; 32]), diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 429e0df7..4978c13b 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -10,46 +10,39 @@ use std::path::PathBuf; +pub use account_manager::AccountIdentity; use anyhow::{Context as _, Result}; use bip39::Mnemonic; use common::{HashType, transaction::NSSATransaction}; use config::WalletConfig; use key_protocol::key_management::key_tree::chain_index::ChainIndex; -use keycard_wallet::KeycardWallet; use log::info; use nssa::{ - Account, AccountId, PrivacyPreservingTransaction, PublicKey, PublicTransaction, Signature, + Account, AccountId, PrivacyPreservingTransaction, privacy_preserving_transaction::{ circuit::ProgramWithDependencies, message::EncryptedAccountData, }, - program::Program, - public_transaction::WitnessSet as PublicWitnessSet, }; use nssa_core::{ - Commitment, MembershipProof, SharedSecretKey, - account::{AccountWithMetadata, Nonce}, - program::InstructionData, + Commitment, MembershipProof, SharedSecretKey, account::Nonce, program::InstructionData, }; -pub use privacy_preserving_tx::PrivacyPreservingAccount; use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder}; use storage::Storage; use tokio::io::AsyncWriteExt as _; use crate::{ account::{AccountIdWithPrivacy, Label}, - cli::CliAccountMention, config::WalletConfigOverrides, poller::TxPoller, - signing::SigningGroup, storage::key_chain::SharedAccountEntry, }; pub mod account; +mod account_manager; pub mod cli; pub mod config; pub mod helperfunctions; pub mod poller; -mod privacy_preserving_tx; pub mod program_facades; pub mod signing; pub mod storage; @@ -295,13 +288,10 @@ impl WalletCore { self.storage.key_chain_mut().set_sealing_secret_key(key); } - /// Resolve an `AccountId` to the appropriate `PrivacyPreservingAccount` variant. + /// Resolve an `AccountId` to the appropriate `AccountIdentity` variant. /// Checks the key tree first, then shared private accounts. #[must_use] - pub fn resolve_private_account( - &self, - account_id: nssa::AccountId, - ) -> Option { + pub fn resolve_private_account(&self, account_id: nssa::AccountId) -> Option { // Check key tree first if self .storage @@ -309,7 +299,7 @@ impl WalletCore { .private_account(account_id) .is_some() { - return Some(PrivacyPreservingAccount::PrivateOwned(account_id)); + return Some(AccountIdentity::PrivateOwned(account_id)); } // Check shared private accounts @@ -322,9 +312,9 @@ impl WalletCore { .key_chain() .group_key_holder(&entry.group_label)?; - if let (Some(pda_seed), Some(program_id)) = (entry.pda_seed, entry.pda_program_id) { + if let (Some(pda_seed), Some(program_id)) = (entry.pda_seed, entry.authority_program_id) { let keys = holder.derive_keys_for_pda(&program_id, &pda_seed); - Some(PrivacyPreservingAccount::PrivatePdaShared { + Some(AccountIdentity::PrivatePdaShared { account_id, nsk: keys.nullifier_secret_key, npk: keys.generate_nullifier_public_key(), @@ -341,7 +331,7 @@ impl WalletCore { result }; let keys = holder.derive_keys_for_shared_account(&derivation_seed); - Some(PrivacyPreservingAccount::PrivateShared { + Some(AccountIdentity::PrivateShared { nsk: keys.nullifier_secret_key, npk: keys.generate_nullifier_public_key(), vpk: keys.generate_viewing_public_key(), @@ -365,7 +355,7 @@ impl WalletCore { group_label: Label, identifier: nssa_core::Identifier, pda_seed: Option, - pda_program_id: Option, + authority_program_id: Option, ) { self.storage.key_chain_mut().insert_shared_private_account( account_id, @@ -373,7 +363,7 @@ impl WalletCore { group_label, identifier, pda_seed, - pda_program_id, + authority_program_id, account: Account::default(), }, ); @@ -564,145 +554,28 @@ impl WalletCore { Ok(()) } - /// Send a public transaction, fetching nonces automatically from - /// [`SigningGroup::signing_ids`]. - pub async fn send_public_tx( - &self, - program: &Program, - account_ids: Vec, - instruction: T, - groups: SigningGroup, - ) -> Result { - let nonces = self - .get_accounts_nonces(groups.signing_ids()) - .await - .map_err(ExecutionFailureKind::SequencerError)?; - self.send_public_tx_with_nonces(program, account_ids, nonces, instruction, groups) - .await - } - - /// Send a public transaction with caller-supplied nonces. - /// - /// Use this when the caller needs to assemble or augment nonces before submission - /// (e.g. injecting a keycard account nonce that was fetched separately). - pub async fn send_public_tx_with_nonces( - &self, - program: &Program, - account_ids: Vec, - nonces: Vec, - instruction: T, - groups: SigningGroup, - ) -> Result { - let message = nssa::public_transaction::Message::try_new( - program.id(), - account_ids, - nonces, - instruction, - )?; - - let pin = if groups.needs_pin() { - crate::helperfunctions::read_pin() - .map_err(ExecutionFailureKind::from_anyhow)? - .as_str() - .to_owned() - } else { - String::new() - }; - - let sigs = groups - .sign_all(&message.hash(), &pin) - .map_err(ExecutionFailureKind::from_anyhow)?; - - let tx = PublicTransaction::new(message, PublicWitnessSet::from_raw_parts(sigs)); - Ok(self - .sequencer_client - .send_transaction(NSSATransaction::Public(tx)) - .await?) - } - pub async fn send_privacy_preserving_tx( &self, - accounts: Vec, + accounts: Vec, instruction_data: InstructionData, program: &ProgramWithDependencies, - mention: Option<&CliAccountMention>, ) -> Result<(HashType, Vec), ExecutionFailureKind> { - self.send_privacy_preserving_tx_with_pre_check( - accounts, - instruction_data, - program, - |_| Ok(()), - mention, - ) + self.send_privacy_preserving_tx_with_pre_check(accounts, instruction_data, program, |_| { + Ok(()) + }) .await } pub async fn send_privacy_preserving_tx_with_pre_check( &self, - accounts: Vec, + accounts: Vec, instruction_data: InstructionData, program: &ProgramWithDependencies, tx_pre_check: impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>, - mention: Option<&CliAccountMention>, ) -> Result<(HashType, Vec), ExecutionFailureKind> { - let acc_manager = privacy_preserving_tx::AccountManager::new(self, accounts).await?; + let acc_manager = account_manager::AccountManager::new(self, accounts).await?; - let mut pre_states = acc_manager.pre_states(); - - let (keycard_account, keycard_pin, keycard_path) = if let Some(key_path_str) = - mention.and_then(CliAccountMention::key_path) - { - let pin = crate::helperfunctions::read_pin().map_err(|e| { - ExecutionFailureKind::KeycardError(pyo3::PyErr::new::< - pyo3::exceptions::PyRuntimeError, - _, - >(e.to_string())) - })?; - let account_id_str = - KeycardWallet::get_public_account_id_for_path_with_connect(&pin, key_path_str)?; - let account_id: AccountId = match account_id_str - .parse::() - .expect("`wallet::lib::send_privacy_preserving_tx_with_pre_check`: invalid account id parsed") - { - AccountIdWithPrivacy::Public(id) | AccountIdWithPrivacy::Private(id) => id, - }; - let account = self - .get_account_public(account_id) - .await - .expect("`wallet::lib::send_privacy_preserving_tx_with_pre_check`: unable to retrieve public account"); - let pin_str = pin.as_str().to_owned(); - ( - Some(AccountWithMetadata { - account, - is_authorized: true, - account_id, - }), - Some(pin_str), - Some(key_path_str.to_owned()), - ) - } else { - (None, None, None) - }; - - let mut nonces: Vec = acc_manager.public_account_nonces().into_iter().collect(); - - let mut account_ids: Vec = acc_manager.public_account_ids(); - - if let Some(acc) = keycard_account.as_ref() { - if acc_manager.public_account_ids().contains(&acc.account_id) { - if let Some(pre) = pre_states - .iter_mut() - .find(|p| p.account_id == acc.account_id) - { - pre.is_authorized = true; - } - nonces.push(acc.account.nonce); - } else { - nonces.push(acc.account.nonce); - account_ids.push(acc.account_id); - pre_states.push(acc.clone()); - } - } + let pre_states = acc_manager.pre_states(); tx_pre_check( &pre_states @@ -717,54 +590,30 @@ impl WalletCore { instruction_data, acc_manager.account_identities(), &program.to_owned(), - ) - .unwrap(); + )?; let message = nssa::privacy_preserving_transaction::message::Message::try_from_circuit_output( - account_ids, - nonces, + acc_manager.public_account_ids(), + acc_manager.public_account_nonces(), private_account_keys .iter() .map(|keys| (keys.npk, keys.vpk.clone(), keys.epk.clone())) .collect(), output, - ) - .unwrap(); + )?; + + let message_hash = message.hash(); + let signatures_public_keys = acc_manager + .sign_message(message_hash) + .map_err(ExecutionFailureKind::from_anyhow)?; let witness_set = - if let (Some(pin), Some(path)) = (keycard_pin.as_deref(), keycard_path.as_deref()) { - let hash = message.hash(); - let local_auth = acc_manager.public_account_auth(); - let mut sigs: Vec<(Signature, PublicKey)> = local_auth - .iter() - .map(|&key| { - ( - Signature::new(key, &hash), - PublicKey::new_from_private_key(key), - ) - }) - .collect(); - let keycard_sig = pyo3::Python::with_gil(|py| { - let mut ctx = crate::signing::KeycardSessionContext::new(pin); - let result = ctx - .get_or_connect(py) - .and_then(|w| w.sign_message_for_path(py, path, &hash)); - ctx.close(py); - result - }) - .map_err(ExecutionFailureKind::KeycardError)?; - sigs.push(keycard_sig); - nssa::privacy_preserving_transaction::witness_set::WitnessSet::from_raw_parts( - sigs, proof, - ) - } else { - nssa::privacy_preserving_transaction::witness_set::WitnessSet::for_message( - &message, - proof, - &acc_manager.public_account_auth(), - ) - }; + nssa::privacy_preserving_transaction::witness_set::WitnessSet::from_raw_parts( + signatures_public_keys, + proof, + ); + let tx = PrivacyPreservingTransaction::new(message, witness_set); let shared_secrets: Vec<_> = private_account_keys @@ -780,6 +629,69 @@ impl WalletCore { )) } + pub async fn send_pub_tx( + &self, + accounts: Vec, + instruction_data: InstructionData, + program: &ProgramWithDependencies, + ) -> Result { + self.send_pub_tx_with_pre_check(accounts, instruction_data, program, |_| Ok(())) + .await + } + + pub async fn send_pub_tx_with_pre_check( + &self, + accounts: Vec, + instruction_data: InstructionData, + program: &ProgramWithDependencies, + tx_pre_check: impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>, + ) -> Result { + // Public transaction, all accounts must be public + if accounts.iter().any(AccountIdentity::is_private) { + return Err(ExecutionFailureKind::TransactionBuildError( + nssa::error::NssaError::InvalidInput( + "Private accounts are not allowed in public transactions".to_owned(), + ), + )); + } + + let acc_manager = account_manager::AccountManager::new(self, accounts).await?; + + let pre_states = acc_manager.pre_states(); + tx_pre_check( + &pre_states + .iter() + .map(|pre| &pre.account) + .collect::>(), + )?; + + let account_ids = acc_manager.public_account_ids(); + let program_id = program.program.id(); + let nonces = acc_manager.public_account_nonces(); + + let message = nssa::public_transaction::Message::new_preserialized( + program_id, + account_ids, + nonces, + instruction_data, + ); + + let message_hash = message.hash(); + let signatures_public_keys = acc_manager + .sign_message(message_hash) + .map_err(ExecutionFailureKind::from_anyhow)?; + + let witness_set = + nssa::public_transaction::WitnessSet::from_raw_parts(signatures_public_keys); + + let tx = nssa::public_transaction::PublicTransaction::new(message, witness_set); + + Ok(self + .sequencer_client + .send_transaction(NSSATransaction::Public(tx)) + .await?) + } + pub async fn sync_to_latest_block(&mut self) -> Result { let latest_block_id = self.sequencer_client.get_last_block_id().await?; println!("Latest block is {latest_block_id}"); @@ -899,7 +811,7 @@ impl WalletCore { .key_chain() .group_key_holder(&entry.group_label)?; - let keys = match (&entry.pda_seed, &entry.pda_program_id) { + let keys = match (&entry.pda_seed, &entry.authority_program_id) { (Some(pda_seed), Some(program_id)) => { holder.derive_keys_for_pda(program_id, pda_seed) } diff --git a/wallet/src/program_facades/amm.rs b/wallet/src/program_facades/amm.rs index 0a3fef32..b47b8fdd 100644 --- a/wallet/src/program_facades/amm.rs +++ b/wallet/src/program_facades/amm.rs @@ -3,7 +3,7 @@ use common::HashType; use nssa::{AccountId, program::Program}; use token_core::TokenHolding; -use crate::{ExecutionFailureKind, WalletCore, cli::CliAccountMention, signing::SigningGroup}; +use crate::{AccountIdentity, ExecutionFailureKind, WalletCore, cli::CliAccountMention}; pub struct Amm<'wallet>(pub &'wallet WalletCore); impl Amm<'_> { @@ -15,18 +15,36 @@ impl Amm<'_> { user_holding_lp: AccountId, balance_a: u128, balance_b: u128, - a_mention: &CliAccountMention, - b_mention: &CliAccountMention, - lp_mention: &CliAccountMention, + user_holding_a_mention: &CliAccountMention, + user_holding_b_mention: &CliAccountMention, + user_holding_lp_mention: &CliAccountMention, ) -> Result { + let user_holding_a_identity = user_holding_a_mention.key_path().map_or( + AccountIdentity::Public(user_holding_a), + |key_path| AccountIdentity::PublicKeycard { + account_id: user_holding_a, + key_path: key_path.to_owned(), + }, + ); + + let user_holding_b_identity = user_holding_b_mention.key_path().map_or( + AccountIdentity::Public(user_holding_b), + |key_path| AccountIdentity::PublicKeycard { + account_id: user_holding_b, + key_path: key_path.to_owned(), + }, + ); + + let user_holding_lp_identity = user_holding_lp_mention.key_path().map_or( + AccountIdentity::Public(user_holding_lp), + |key_path| AccountIdentity::PublicKeycard { + account_id: user_holding_lp, + key_path: key_path.to_owned(), + }, + ); + let program = Program::amm(); let amm_program_id = Program::amm().id(); - let instruction = amm_core::Instruction::NewDefinition { - token_a_amount: balance_a, - token_b_amount: balance_b, - amm_program_id, - }; - let user_a_acc = self .0 .get_account_public(user_holding_a) @@ -50,26 +68,28 @@ impl Amm<'_> { let vault_holding_a = compute_vault_pda(amm_program_id, amm_pool, definition_token_a_id); let vault_holding_b = compute_vault_pda(amm_program_id, amm_pool, definition_token_b_id); let pool_lp = compute_liquidity_token_pda(amm_program_id, amm_pool); - - let account_ids = vec![ - amm_pool, - vault_holding_a, - vault_holding_b, - pool_lp, - user_holding_a, - user_holding_b, - user_holding_lp, - ]; - - let mut groups = SigningGroup::new(); - groups - .add_required(a_mention, user_holding_a, self.0) - .and_then(|()| groups.add_required(b_mention, user_holding_b, self.0)) - .and_then(|()| groups.add_optional(lp_mention, user_holding_lp, self.0)) - .map_err(ExecutionFailureKind::from_anyhow)?; + let instruction = amm_core::Instruction::NewDefinition { + token_a_amount: balance_a, + token_b_amount: balance_b, + amm_program_id, + }; + let instruction_data = + Program::serialize_instruction(instruction).expect("Instruction should serialize"); self.0 - .send_public_tx(&program, account_ids, instruction, groups) + .send_pub_tx( + vec![ + AccountIdentity::PublicNoSign(amm_pool), + AccountIdentity::PublicNoSign(vault_holding_a), + AccountIdentity::PublicNoSign(vault_holding_b), + AccountIdentity::PublicNoSign(pool_lp), + user_holding_a_identity, + user_holding_b_identity, + user_holding_lp_identity, + ], + instruction_data, + &program.into(), + ) .await } @@ -81,17 +101,11 @@ impl Amm<'_> { swap_amount_in: u128, min_amount_out: u128, token_definition_id_in: AccountId, - a_mention: &CliAccountMention, - b_mention: &CliAccountMention, + user_holding_a_mention: &CliAccountMention, + user_holding_b_mention: &CliAccountMention, ) -> Result { - let instruction = amm_core::Instruction::SwapExactInput { - swap_amount_in, - min_amount_out, - token_definition_id_in, - }; let program = Program::amm(); let amm_program_id = Program::amm().id(); - let user_a_acc = self .0 .get_account_public(user_holding_a) @@ -114,31 +128,58 @@ impl Amm<'_> { compute_pool_pda(amm_program_id, definition_token_a_id, definition_token_b_id); let vault_holding_a = compute_vault_pda(amm_program_id, amm_pool, definition_token_a_id); let vault_holding_b = compute_vault_pda(amm_program_id, amm_pool, definition_token_b_id); + let instruction = amm_core::Instruction::SwapExactInput { + swap_amount_in, + min_amount_out, + token_definition_id_in, + }; + let instruction_data = + Program::serialize_instruction(instruction).expect("Instruction should serialize"); - let account_ids = vec![ - amm_pool, - vault_holding_a, - vault_holding_b, - user_holding_a, - user_holding_b, - ]; - - let (account_id_auth, seller_mention) = if definition_token_a_id == token_definition_id_in { - (user_holding_a, a_mention) - } else if definition_token_b_id == token_definition_id_in { - (user_holding_b, b_mention) - } else { + if (token_definition_id_in != definition_token_a_id) + && (token_definition_id_in != definition_token_b_id) + { return Err(ExecutionFailureKind::AccountDataError( token_definition_id_in, )); + } + + let user_a_signing_identity = if token_definition_id_in == definition_token_a_id { + user_holding_a_mention.key_path().map_or( + AccountIdentity::Public(user_holding_a), + |key_path| AccountIdentity::PublicKeycard { + account_id: user_holding_a, + key_path: key_path.to_owned(), + }, + ) + } else { + AccountIdentity::PublicNoSign(user_holding_a) + }; + + let user_b_signing_identity = if token_definition_id_in == definition_token_b_id { + user_holding_b_mention.key_path().map_or( + AccountIdentity::Public(user_holding_b), + |key_path| AccountIdentity::PublicKeycard { + account_id: user_holding_b, + key_path: key_path.to_owned(), + }, + ) + } else { + AccountIdentity::PublicNoSign(user_holding_b) }; - let mut groups = SigningGroup::new(); - groups - .add_required(seller_mention, account_id_auth, self.0) - .map_err(ExecutionFailureKind::from_anyhow)?; self.0 - .send_public_tx(&program, account_ids, instruction, groups) + .send_pub_tx( + vec![ + AccountIdentity::PublicNoSign(amm_pool), + AccountIdentity::PublicNoSign(vault_holding_a), + AccountIdentity::PublicNoSign(vault_holding_b), + user_a_signing_identity, + user_b_signing_identity, + ], + instruction_data, + &program.into(), + ) .await } @@ -150,17 +191,11 @@ impl Amm<'_> { exact_amount_out: u128, max_amount_in: u128, token_definition_id_in: AccountId, - a_mention: &CliAccountMention, - b_mention: &CliAccountMention, + user_holding_a_mention: &CliAccountMention, + user_holding_b_mention: &CliAccountMention, ) -> Result { - let instruction = amm_core::Instruction::SwapExactOutput { - exact_amount_out, - max_amount_in, - token_definition_id_in, - }; let program = Program::amm(); let amm_program_id = Program::amm().id(); - let user_a_acc = self .0 .get_account_public(user_holding_a) @@ -183,31 +218,58 @@ impl Amm<'_> { compute_pool_pda(amm_program_id, definition_token_a_id, definition_token_b_id); let vault_holding_a = compute_vault_pda(amm_program_id, amm_pool, definition_token_a_id); let vault_holding_b = compute_vault_pda(amm_program_id, amm_pool, definition_token_b_id); + let instruction = amm_core::Instruction::SwapExactOutput { + exact_amount_out, + max_amount_in, + token_definition_id_in, + }; + let instruction_data = + Program::serialize_instruction(instruction).expect("Instruction should serialize"); - let account_ids = vec![ - amm_pool, - vault_holding_a, - vault_holding_b, - user_holding_a, - user_holding_b, - ]; - - let (account_id_auth, seller_mention) = if definition_token_a_id == token_definition_id_in { - (user_holding_a, a_mention) - } else if definition_token_b_id == token_definition_id_in { - (user_holding_b, b_mention) - } else { + if (token_definition_id_in != definition_token_a_id) + && (token_definition_id_in != definition_token_b_id) + { return Err(ExecutionFailureKind::AccountDataError( token_definition_id_in, )); + } + + let user_a_signing_identity = if token_definition_id_in == definition_token_a_id { + user_holding_a_mention.key_path().map_or( + AccountIdentity::Public(user_holding_a), + |key_path| AccountIdentity::PublicKeycard { + account_id: user_holding_a, + key_path: key_path.to_owned(), + }, + ) + } else { + AccountIdentity::PublicNoSign(user_holding_a) + }; + + let user_b_signing_identity = if token_definition_id_in == definition_token_b_id { + user_holding_b_mention.key_path().map_or( + AccountIdentity::Public(user_holding_b), + |key_path| AccountIdentity::PublicKeycard { + account_id: user_holding_b, + key_path: key_path.to_owned(), + }, + ) + } else { + AccountIdentity::PublicNoSign(user_holding_b) }; - let mut groups = SigningGroup::new(); - groups - .add_required(seller_mention, account_id_auth, self.0) - .map_err(ExecutionFailureKind::from_anyhow)?; self.0 - .send_public_tx(&program, account_ids, instruction, groups) + .send_pub_tx( + vec![ + AccountIdentity::PublicNoSign(amm_pool), + AccountIdentity::PublicNoSign(vault_holding_a), + AccountIdentity::PublicNoSign(vault_holding_b), + user_a_signing_identity, + user_b_signing_identity, + ], + instruction_data, + &program.into(), + ) .await } @@ -220,18 +282,36 @@ impl Amm<'_> { min_amount_liquidity: u128, max_amount_to_add_token_a: u128, max_amount_to_add_token_b: u128, - a_mention: &CliAccountMention, - b_mention: &CliAccountMention, - lp_mention: &CliAccountMention, + user_holding_a_mention: &CliAccountMention, + user_holding_b_mention: &CliAccountMention, + user_holding_lp_mention: &CliAccountMention, ) -> Result { - let instruction = amm_core::Instruction::AddLiquidity { - min_amount_liquidity, - max_amount_to_add_token_a, - max_amount_to_add_token_b, - }; + let user_holding_a_identity = user_holding_a_mention.key_path().map_or( + AccountIdentity::Public(user_holding_a), + |key_path| AccountIdentity::PublicKeycard { + account_id: user_holding_a, + key_path: key_path.to_owned(), + }, + ); + + let user_holding_b_identity = user_holding_b_mention.key_path().map_or( + AccountIdentity::Public(user_holding_b), + |key_path| AccountIdentity::PublicKeycard { + account_id: user_holding_b, + key_path: key_path.to_owned(), + }, + ); + + let user_holding_lp_identity = user_holding_lp_mention.key_path().map_or( + AccountIdentity::Public(user_holding_lp), + |key_path| AccountIdentity::PublicKeycard { + account_id: user_holding_lp, + key_path: key_path.to_owned(), + }, + ); + let program = Program::amm(); let amm_program_id = Program::amm().id(); - let user_a_acc = self .0 .get_account_public(user_holding_a) @@ -255,26 +335,28 @@ impl Amm<'_> { let vault_holding_a = compute_vault_pda(amm_program_id, amm_pool, definition_token_a_id); let vault_holding_b = compute_vault_pda(amm_program_id, amm_pool, definition_token_b_id); let pool_lp = compute_liquidity_token_pda(amm_program_id, amm_pool); - - let account_ids = vec![ - amm_pool, - vault_holding_a, - vault_holding_b, - pool_lp, - user_holding_a, - user_holding_b, - user_holding_lp, - ]; - - let mut groups = SigningGroup::new(); - groups - .add_required(a_mention, user_holding_a, self.0) - .and_then(|()| groups.add_required(b_mention, user_holding_b, self.0)) - .and_then(|()| groups.add_optional(lp_mention, user_holding_lp, self.0)) - .map_err(ExecutionFailureKind::from_anyhow)?; + let instruction = amm_core::Instruction::AddLiquidity { + min_amount_liquidity, + max_amount_to_add_token_a, + max_amount_to_add_token_b, + }; + let instruction_data = + Program::serialize_instruction(instruction).expect("Instruction should serialize"); self.0 - .send_public_tx(&program, account_ids, instruction, groups) + .send_pub_tx( + vec![ + AccountIdentity::PublicNoSign(amm_pool), + AccountIdentity::PublicNoSign(vault_holding_a), + AccountIdentity::PublicNoSign(vault_holding_b), + AccountIdentity::PublicNoSign(pool_lp), + user_holding_a_identity, + user_holding_b_identity, + user_holding_lp_identity, + ], + instruction_data, + &program.into(), + ) .await } @@ -287,16 +369,18 @@ impl Amm<'_> { remove_liquidity_amount: u128, min_amount_to_remove_token_a: u128, min_amount_to_remove_token_b: u128, - lp_mention: &CliAccountMention, + user_holding_lp_mention: &CliAccountMention, ) -> Result { - let instruction = amm_core::Instruction::RemoveLiquidity { - remove_liquidity_amount, - min_amount_to_remove_token_a, - min_amount_to_remove_token_b, - }; + let user_holding_lp_identity = user_holding_lp_mention.key_path().map_or( + AccountIdentity::Public(user_holding_lp), + |key_path| AccountIdentity::PublicKeycard { + account_id: user_holding_lp, + key_path: key_path.to_owned(), + }, + ); + let program = Program::amm(); let amm_program_id = Program::amm().id(); - let user_a_acc = self .0 .get_account_public(user_holding_a) @@ -320,23 +404,28 @@ impl Amm<'_> { let vault_holding_a = compute_vault_pda(amm_program_id, amm_pool, definition_token_a_id); let vault_holding_b = compute_vault_pda(amm_program_id, amm_pool, definition_token_b_id); let pool_lp = compute_liquidity_token_pda(amm_program_id, amm_pool); + let instruction = amm_core::Instruction::RemoveLiquidity { + remove_liquidity_amount, + min_amount_to_remove_token_a, + min_amount_to_remove_token_b, + }; + let instruction_data = + Program::serialize_instruction(instruction).expect("Instruction should serialize"); - let account_ids = vec![ - amm_pool, - vault_holding_a, - vault_holding_b, - pool_lp, - user_holding_a, - user_holding_b, - user_holding_lp, - ]; - - let mut groups = SigningGroup::new(); - groups - .add_required(lp_mention, user_holding_lp, self.0) - .map_err(ExecutionFailureKind::from_anyhow)?; self.0 - .send_public_tx(&program, account_ids, instruction, groups) + .send_pub_tx( + vec![ + AccountIdentity::PublicNoSign(amm_pool), + AccountIdentity::PublicNoSign(vault_holding_a), + AccountIdentity::PublicNoSign(vault_holding_b), + AccountIdentity::PublicNoSign(pool_lp), + AccountIdentity::PublicNoSign(user_holding_a), + AccountIdentity::PublicNoSign(user_holding_b), + user_holding_lp_identity, + ], + instruction_data, + &program.into(), + ) .await } } diff --git a/wallet/src/program_facades/ata.rs b/wallet/src/program_facades/ata.rs index 09f11eea..d3a24fa3 100644 --- a/wallet/src/program_facades/ata.rs +++ b/wallet/src/program_facades/ata.rs @@ -7,10 +7,7 @@ use nssa::{ }; use nssa_core::SharedSecretKey; -use crate::{ - ExecutionFailureKind, PrivacyPreservingAccount, WalletCore, cli::CliAccountMention, - signing::SigningGroup, -}; +use crate::{AccountIdentity, ExecutionFailureKind, WalletCore, cli::CliAccountMention}; pub struct Ata<'wallet>(pub &'wallet WalletCore); @@ -21,22 +18,36 @@ impl Ata<'_> { definition_id: AccountId, owner_mention: &CliAccountMention, ) -> Result { + let owner_identity = + owner_mention + .key_path() + .map_or(AccountIdentity::Public(owner_id), |key_path| { + AccountIdentity::PublicKeycard { + account_id: owner_id, + key_path: key_path.to_owned(), + } + }); + let program = Program::ata(); let ata_program_id = program.id(); let ata_id = get_associated_token_account_id( &ata_program_id, &compute_ata_seed(owner_id, definition_id), ); - - let account_ids = vec![owner_id, definition_id, ata_id]; let instruction = ata_core::Instruction::Create { ata_program_id }; + let instruction_data = + Program::serialize_instruction(instruction).expect("Instruction should serialize"); - let mut groups = SigningGroup::new(); - groups - .add_required(owner_mention, owner_id, self.0) - .map_err(ExecutionFailureKind::from_anyhow)?; self.0 - .send_public_tx(&program, account_ids, instruction, groups) + .send_pub_tx( + vec![ + owner_identity, + AccountIdentity::PublicNoSign(definition_id), + AccountIdentity::PublicNoSign(ata_id), + ], + instruction_data, + &program.into(), + ) .await } @@ -48,25 +59,39 @@ impl Ata<'_> { amount: u128, owner_mention: &CliAccountMention, ) -> Result { + let owner_identity = + owner_mention + .key_path() + .map_or(AccountIdentity::Public(owner_id), |key_path| { + AccountIdentity::PublicKeycard { + account_id: owner_id, + key_path: key_path.to_owned(), + } + }); + let program = Program::ata(); let ata_program_id = program.id(); let sender_ata_id = get_associated_token_account_id( &ata_program_id, &compute_ata_seed(owner_id, definition_id), ); - - let account_ids = vec![owner_id, sender_ata_id, recipient_id]; let instruction = ata_core::Instruction::Transfer { ata_program_id, amount, }; + let instruction_data = + Program::serialize_instruction(instruction).expect("Instruction should serialize"); - let mut groups = SigningGroup::new(); - groups - .add_required(owner_mention, owner_id, self.0) - .map_err(ExecutionFailureKind::from_anyhow)?; self.0 - .send_public_tx(&program, account_ids, instruction, groups) + .send_pub_tx( + vec![ + owner_identity, + AccountIdentity::PublicNoSign(sender_ata_id), + AccountIdentity::PublicNoSign(recipient_id), + ], + instruction_data, + &program.into(), + ) .await } @@ -77,25 +102,39 @@ impl Ata<'_> { amount: u128, owner_mention: &CliAccountMention, ) -> Result { + let owner_identity = + owner_mention + .key_path() + .map_or(AccountIdentity::Public(owner_id), |key_path| { + AccountIdentity::PublicKeycard { + account_id: owner_id, + key_path: key_path.to_owned(), + } + }); + let program = Program::ata(); let ata_program_id = program.id(); let holder_ata_id = get_associated_token_account_id( &ata_program_id, &compute_ata_seed(owner_id, definition_id), ); - - let account_ids = vec![owner_id, holder_ata_id, definition_id]; let instruction = ata_core::Instruction::Burn { ata_program_id, amount, }; + let instruction_data = + Program::serialize_instruction(instruction).expect("Instruction should serialize"); - let mut groups = SigningGroup::new(); - groups - .add_required(owner_mention, owner_id, self.0) - .map_err(ExecutionFailureKind::from_anyhow)?; self.0 - .send_public_tx(&program, account_ids, instruction, groups) + .send_pub_tx( + vec![ + owner_identity, + AccountIdentity::PublicNoSign(holder_ata_id), + AccountIdentity::PublicNoSign(definition_id), + ], + instruction_data, + &program.into(), + ) .await } @@ -118,17 +157,12 @@ impl Ata<'_> { self.0 .resolve_private_account(owner_id) .ok_or(ExecutionFailureKind::KeyNotFoundError)?, - PrivacyPreservingAccount::Public(definition_id), - PrivacyPreservingAccount::Public(ata_id), + AccountIdentity::Public(definition_id), + AccountIdentity::Public(ata_id), ]; self.0 - .send_privacy_preserving_tx( - accounts, - instruction_data, - &ata_with_token_dependency(), - None, - ) + .send_privacy_preserving_tx(accounts, instruction_data, &ata_with_token_dependency()) .await .map(|(hash, mut secrets)| { let secret = secrets.pop().expect("expected owner's secret"); @@ -160,17 +194,12 @@ impl Ata<'_> { self.0 .resolve_private_account(owner_id) .ok_or(ExecutionFailureKind::KeyNotFoundError)?, - PrivacyPreservingAccount::Public(sender_ata_id), - PrivacyPreservingAccount::Public(recipient_id), + AccountIdentity::Public(sender_ata_id), + AccountIdentity::Public(recipient_id), ]; self.0 - .send_privacy_preserving_tx( - accounts, - instruction_data, - &ata_with_token_dependency(), - None, - ) + .send_privacy_preserving_tx(accounts, instruction_data, &ata_with_token_dependency()) .await .map(|(hash, mut secrets)| { let secret = secrets.pop().expect("expected owner's secret"); @@ -201,17 +230,12 @@ impl Ata<'_> { self.0 .resolve_private_account(owner_id) .ok_or(ExecutionFailureKind::KeyNotFoundError)?, - PrivacyPreservingAccount::Public(holder_ata_id), - PrivacyPreservingAccount::Public(definition_id), + AccountIdentity::Public(holder_ata_id), + AccountIdentity::Public(definition_id), ]; self.0 - .send_privacy_preserving_tx( - accounts, - instruction_data, - &ata_with_token_dependency(), - None, - ) + .send_privacy_preserving_tx(accounts, instruction_data, &ata_with_token_dependency()) .await .map(|(hash, mut secrets)| { let secret = secrets.pop().expect("expected owner's secret"); diff --git a/wallet/src/program_facades/native_token_transfer/deshielded.rs b/wallet/src/program_facades/native_token_transfer/deshielded.rs index 5a941d24..31374f99 100644 --- a/wallet/src/program_facades/native_token_transfer/deshielded.rs +++ b/wallet/src/program_facades/native_token_transfer/deshielded.rs @@ -2,7 +2,7 @@ use common::HashType; use nssa::AccountId; use super::{NativeTokenTransfer, auth_transfer_preparation}; -use crate::{ExecutionFailureKind, PrivacyPreservingAccount}; +use crate::{AccountIdentity, ExecutionFailureKind}; impl NativeTokenTransfer<'_> { pub async fn send_deshielded_transfer( @@ -19,12 +19,11 @@ impl NativeTokenTransfer<'_> { self.0 .resolve_private_account(from) .ok_or(ExecutionFailureKind::KeyNotFoundError)?, - PrivacyPreservingAccount::Public(to), + AccountIdentity::Public(to), ], instruction_data, &program.into(), tx_pre_check, - None, ) .await .map(|(resp, secrets)| { diff --git a/wallet/src/program_facades/native_token_transfer/private.rs b/wallet/src/program_facades/native_token_transfer/private.rs index c940dfb6..481e4a5f 100644 --- a/wallet/src/program_facades/native_token_transfer/private.rs +++ b/wallet/src/program_facades/native_token_transfer/private.rs @@ -5,7 +5,7 @@ use nssa::{AccountId, program::Program}; use nssa_core::{Identifier, NullifierPublicKey, SharedSecretKey, encryption::ViewingPublicKey}; use super::{NativeTokenTransfer, auth_transfer_preparation}; -use crate::{ExecutionFailureKind, PrivacyPreservingAccount}; +use crate::{AccountIdentity, ExecutionFailureKind}; impl NativeTokenTransfer<'_> { pub async fn register_account_private( @@ -24,7 +24,6 @@ impl NativeTokenTransfer<'_> { vec![account], Program::serialize_instruction(instruction).unwrap(), &Program::authenticated_transfer_program().into(), - None, ) .await .map(|(resp, secrets)| { @@ -50,7 +49,7 @@ impl NativeTokenTransfer<'_> { self.0 .resolve_private_account(from) .ok_or(ExecutionFailureKind::KeyNotFoundError)?, - PrivacyPreservingAccount::PrivateForeign { + AccountIdentity::PrivateForeign { npk: to_npk, vpk: to_vpk, identifier: to_identifier, @@ -59,7 +58,6 @@ impl NativeTokenTransfer<'_> { instruction_data, &program.into(), tx_pre_check, - None, ) .await .map(|(resp, secrets)| { @@ -93,7 +91,6 @@ impl NativeTokenTransfer<'_> { instruction_data, &program.into(), tx_pre_check, - None, ) .await .map(|(resp, secrets)| { diff --git a/wallet/src/program_facades/native_token_transfer/public.rs b/wallet/src/program_facades/native_token_transfer/public.rs index 68c8e0be..8d441b2b 100644 --- a/wallet/src/program_facades/native_token_transfer/public.rs +++ b/wallet/src/program_facades/native_token_transfer/public.rs @@ -3,7 +3,10 @@ use common::HashType; use nssa::{AccountId, program::Program}; use super::NativeTokenTransfer; -use crate::{ExecutionFailureKind, cli::CliAccountMention, signing::SigningGroup}; +use crate::{ + AccountIdentity, ExecutionFailureKind, cli::CliAccountMention, + program_facades::native_token_transfer::auth_transfer_preparation, +}; impl NativeTokenTransfer<'_> { pub async fn send_public_transfer( @@ -14,20 +17,33 @@ impl NativeTokenTransfer<'_> { from_mention: &CliAccountMention, to_mention: &CliAccountMention, ) -> Result { - let mut groups = SigningGroup::new(); - groups - .add_required(from_mention, from, self.0) - .and_then(|()| groups.add_optional(to_mention, to, self.0)) - .map_err(ExecutionFailureKind::from_anyhow)?; + let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move); + + let from_identity = + from_mention + .key_path() + .map_or(AccountIdentity::Public(from), |key_path| { + AccountIdentity::PublicKeycard { + account_id: from, + key_path: key_path.to_owned(), + } + }); + + let to_identity = to_mention + .key_path() + .map_or(AccountIdentity::Public(to), |key_path| { + AccountIdentity::PublicKeycard { + account_id: to, + key_path: key_path.to_owned(), + } + }); self.0 - .send_public_tx( - &Program::authenticated_transfer_program(), - vec![from, to], - AuthTransferInstruction::Transfer { - amount: balance_to_move, - }, - groups, + .send_pub_tx_with_pre_check( + vec![from_identity, to_identity], + instruction_data, + &program.into(), + tx_pre_check, ) .await } @@ -37,18 +53,21 @@ impl NativeTokenTransfer<'_> { from: AccountId, account_mention: &CliAccountMention, ) -> Result { - let mut groups = SigningGroup::new(); - groups - .add_required(account_mention, from, self.0) - .map_err(ExecutionFailureKind::from_anyhow)?; + let from_identity = + account_mention + .key_path() + .map_or(AccountIdentity::Public(from), |key_path| { + AccountIdentity::PublicKeycard { + account_id: from, + key_path: key_path.to_owned(), + } + }); + + let program = Program::authenticated_transfer_program(); + let instruction_data = Program::serialize_instruction(AuthTransferInstruction::Initialize)?; self.0 - .send_public_tx( - &Program::authenticated_transfer_program(), - vec![from], - AuthTransferInstruction::Initialize, - groups, - ) + .send_pub_tx(vec![from_identity], instruction_data, &program.into()) .await } } diff --git a/wallet/src/program_facades/native_token_transfer/shielded.rs b/wallet/src/program_facades/native_token_transfer/shielded.rs index e221e87e..339673fb 100644 --- a/wallet/src/program_facades/native_token_transfer/shielded.rs +++ b/wallet/src/program_facades/native_token_transfer/shielded.rs @@ -3,7 +3,7 @@ use nssa::AccountId; use nssa_core::{Identifier, NullifierPublicKey, SharedSecretKey, encryption::ViewingPublicKey}; use super::{NativeTokenTransfer, auth_transfer_preparation}; -use crate::{ExecutionFailureKind, PrivacyPreservingAccount, cli::CliAccountMention}; +use crate::{AccountIdentity, ExecutionFailureKind, cli::CliAccountMention}; impl NativeTokenTransfer<'_> { pub async fn send_shielded_transfer( @@ -13,11 +13,21 @@ impl NativeTokenTransfer<'_> { balance_to_move: u128, from_mention: &CliAccountMention, ) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> { + let from_identity = + from_mention + .key_path() + .map_or(AccountIdentity::Public(from), |key_path| { + AccountIdentity::PublicKeycard { + account_id: from, + key_path: key_path.to_owned(), + } + }); + let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move); self.0 .send_privacy_preserving_tx_with_pre_check( vec![ - PrivacyPreservingAccount::Public(from), + from_identity, self.0 .resolve_private_account(to) .ok_or(ExecutionFailureKind::KeyNotFoundError)?, @@ -25,7 +35,6 @@ impl NativeTokenTransfer<'_> { instruction_data, &program.into(), tx_pre_check, - Some(from_mention), ) .await .map(|(resp, secrets)| { @@ -46,12 +55,22 @@ impl NativeTokenTransfer<'_> { balance_to_move: u128, from_mention: &CliAccountMention, ) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> { + let from_identity = + from_mention + .key_path() + .map_or(AccountIdentity::Public(from), |key_path| { + AccountIdentity::PublicKeycard { + account_id: from, + key_path: key_path.to_owned(), + } + }); + let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move); self.0 .send_privacy_preserving_tx_with_pre_check( vec![ - PrivacyPreservingAccount::Public(from), - PrivacyPreservingAccount::PrivateForeign { + from_identity, + AccountIdentity::PrivateForeign { npk: to_npk, vpk: to_vpk, identifier: to_identifier, @@ -60,7 +79,6 @@ impl NativeTokenTransfer<'_> { instruction_data, &program.into(), tx_pre_check, - Some(from_mention), ) .await .map(|(resp, secrets)| { diff --git a/wallet/src/program_facades/pinata.rs b/wallet/src/program_facades/pinata.rs index fd37016a..2e40e78b 100644 --- a/wallet/src/program_facades/pinata.rs +++ b/wallet/src/program_facades/pinata.rs @@ -1,9 +1,8 @@ -use common::{HashType, transaction::NSSATransaction}; -use nssa::AccountId; +use common::HashType; +use nssa::{AccountId, program::Program}; use nssa_core::{MembershipProof, SharedSecretKey}; -use sequencer_service_rpc::RpcClient as _; -use crate::{ExecutionFailureKind, PrivacyPreservingAccount, WalletCore}; +use crate::{AccountIdentity, ExecutionFailureKind, WalletCore}; pub struct Pinata<'wallet>(pub &'wallet WalletCore); @@ -14,20 +13,21 @@ impl Pinata<'_> { winner_account_id: AccountId, solution: u128, ) -> Result { - let account_ids = vec![pinata_account_id, winner_account_id]; - let program_id = nssa::program::Program::pinata().id(); - let message = - nssa::public_transaction::Message::try_new(program_id, account_ids, vec![], solution) - .unwrap(); + let program = Program::pinata(); + let instruction = solution; + let instruction_data = + Program::serialize_instruction(instruction).expect("Instruction should serialize"); - let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]); - let tx = nssa::PublicTransaction::new(message, witness_set); - - Ok(self - .0 - .sequencer_client - .send_transaction(NSSATransaction::Public(tx)) - .await?) + self.0 + .send_pub_tx( + vec![ + AccountIdentity::PublicNoSign(pinata_account_id), + AccountIdentity::PublicNoSign(winner_account_id), + ], + instruction_data, + &program.into(), + ) + .await } /// Claim a pinata reward using a privacy-preserving transaction for an already-initialized @@ -55,14 +55,13 @@ impl Pinata<'_> { self.0 .send_privacy_preserving_tx( vec![ - PrivacyPreservingAccount::Public(pinata_account_id), + AccountIdentity::Public(pinata_account_id), self.0 .resolve_private_account(winner_account_id) .ok_or(ExecutionFailureKind::KeyNotFoundError)?, ], nssa::program::Program::serialize_instruction(solution).unwrap(), &nssa::program::Program::pinata().into(), - None, ) .await .map(|(resp, secrets)| { diff --git a/wallet/src/program_facades/token.rs b/wallet/src/program_facades/token.rs index c16b6365..9d327f48 100644 --- a/wallet/src/program_facades/token.rs +++ b/wallet/src/program_facades/token.rs @@ -3,10 +3,7 @@ use nssa::{AccountId, program::Program}; use nssa_core::{Identifier, NullifierPublicKey, SharedSecretKey, encryption::ViewingPublicKey}; use token_core::Instruction; -use crate::{ - ExecutionFailureKind, PrivacyPreservingAccount, WalletCore, cli::CliAccountMention, - signing::SigningGroup, -}; +use crate::{AccountIdentity, ExecutionFailureKind, WalletCore, cli::CliAccountMention}; pub struct Token<'wallet>(pub &'wallet WalletCore); @@ -20,17 +17,33 @@ impl Token<'_> { definition_mention: &CliAccountMention, supply_mention: &CliAccountMention, ) -> Result { - let account_ids = vec![definition_account_id, supply_account_id]; - let instruction = Instruction::NewFungibleDefinition { name, total_supply }; + let definition_identity = definition_mention.key_path().map_or( + AccountIdentity::Public(definition_account_id), + |key_path| AccountIdentity::PublicKeycard { + account_id: definition_account_id, + key_path: key_path.to_owned(), + }, + ); - let mut groups = SigningGroup::new(); - groups - .add_required(definition_mention, definition_account_id, self.0) - .and_then(|()| groups.add_required(supply_mention, supply_account_id, self.0)) - .map_err(ExecutionFailureKind::from_anyhow)?; + let supply_identity = supply_mention.key_path().map_or( + AccountIdentity::Public(supply_account_id), + |key_path| AccountIdentity::PublicKeycard { + account_id: supply_account_id, + key_path: key_path.to_owned(), + }, + ); + + let program = Program::token(); + let instruction = Instruction::NewFungibleDefinition { name, total_supply }; + let instruction_data = + Program::serialize_instruction(instruction).expect("Instruction should serialize"); self.0 - .send_public_tx(&Program::token(), account_ids, instruction, groups) + .send_pub_tx( + vec![definition_identity, supply_identity], + instruction_data, + &program.into(), + ) .await } @@ -48,14 +61,13 @@ impl Token<'_> { self.0 .send_privacy_preserving_tx( vec![ - PrivacyPreservingAccount::Public(definition_account_id), + AccountIdentity::Public(definition_account_id), self.0 .resolve_private_account(supply_account_id) .ok_or(ExecutionFailureKind::KeyNotFoundError)?, ], instruction_data, &Program::token().into(), - None, ) .await .map(|(resp, secrets)| { @@ -84,11 +96,10 @@ impl Token<'_> { self.0 .resolve_private_account(definition_account_id) .ok_or(ExecutionFailureKind::KeyNotFoundError)?, - PrivacyPreservingAccount::Public(supply_account_id), + AccountIdentity::Public(supply_account_id), ], instruction_data, &Program::token().into(), - None, ) .await .map(|(resp, secrets)| { @@ -123,7 +134,6 @@ impl Token<'_> { ], instruction_data, &Program::token().into(), - None, ) .await .map(|(resp, secrets)| { @@ -142,19 +152,35 @@ impl Token<'_> { sender_mention: &CliAccountMention, recipient_mention: &CliAccountMention, ) -> Result { - let account_ids = vec![sender_account_id, recipient_account_id]; + let sender_identity = sender_mention.key_path().map_or( + AccountIdentity::Public(sender_account_id), + |key_path| AccountIdentity::PublicKeycard { + account_id: sender_account_id, + key_path: key_path.to_owned(), + }, + ); + + let recipient_identity = recipient_mention.key_path().map_or( + AccountIdentity::Public(recipient_account_id), + |key_path| AccountIdentity::PublicKeycard { + account_id: recipient_account_id, + key_path: key_path.to_owned(), + }, + ); + + let program = Program::token(); let instruction = Instruction::Transfer { amount_to_transfer: amount, }; - - let mut groups = SigningGroup::new(); - groups - .add_required(sender_mention, sender_account_id, self.0) - .and_then(|()| groups.add_optional(recipient_mention, recipient_account_id, self.0)) - .map_err(ExecutionFailureKind::from_anyhow)?; + let instruction_data = + Program::serialize_instruction(instruction).expect("Instruction should serialize"); self.0 - .send_public_tx(&Program::token(), account_ids, instruction, groups) + .send_pub_tx( + vec![sender_identity, recipient_identity], + instruction_data, + &program.into(), + ) .await } @@ -182,7 +208,6 @@ impl Token<'_> { ], instruction_data, &Program::token().into(), - None, ) .await .map(|(resp, secrets)| { @@ -213,7 +238,7 @@ impl Token<'_> { self.0 .resolve_private_account(sender_account_id) .ok_or(ExecutionFailureKind::KeyNotFoundError)?, - PrivacyPreservingAccount::PrivateForeign { + AccountIdentity::PrivateForeign { npk: recipient_npk, vpk: recipient_vpk, identifier: recipient_identifier, @@ -221,7 +246,6 @@ impl Token<'_> { ], instruction_data, &Program::token().into(), - None, ) .await .map(|(resp, secrets)| { @@ -250,11 +274,10 @@ impl Token<'_> { self.0 .resolve_private_account(sender_account_id) .ok_or(ExecutionFailureKind::KeyNotFoundError)?, - PrivacyPreservingAccount::Public(recipient_account_id), + AccountIdentity::Public(recipient_account_id), ], instruction_data, &Program::token().into(), - None, ) .await .map(|(resp, secrets)| { @@ -273,6 +296,14 @@ impl Token<'_> { amount: u128, sender_mention: &CliAccountMention, ) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> { + let sender_identity = sender_mention.key_path().map_or( + AccountIdentity::Public(sender_account_id), + |key_path| AccountIdentity::PublicKeycard { + account_id: sender_account_id, + key_path: key_path.to_owned(), + }, + ); + let instruction = Instruction::Transfer { amount_to_transfer: amount, }; @@ -281,14 +312,13 @@ impl Token<'_> { self.0 .send_privacy_preserving_tx( vec![ - PrivacyPreservingAccount::Public(sender_account_id), + sender_identity, self.0 .resolve_private_account(recipient_account_id) .ok_or(ExecutionFailureKind::KeyNotFoundError)?, ], instruction_data, &Program::token().into(), - Some(sender_mention), ) .await .map(|(resp, secrets)| { @@ -309,6 +339,14 @@ impl Token<'_> { amount: u128, sender_mention: &CliAccountMention, ) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> { + let sender_identity = sender_mention.key_path().map_or( + AccountIdentity::Public(sender_account_id), + |key_path| AccountIdentity::PublicKeycard { + account_id: sender_account_id, + key_path: key_path.to_owned(), + }, + ); + let instruction = Instruction::Transfer { amount_to_transfer: amount, }; @@ -317,8 +355,8 @@ impl Token<'_> { self.0 .send_privacy_preserving_tx( vec![ - PrivacyPreservingAccount::Public(sender_account_id), - PrivacyPreservingAccount::PrivateForeign { + sender_identity, + AccountIdentity::PrivateForeign { npk: recipient_npk, vpk: recipient_vpk, identifier: recipient_identifier, @@ -326,7 +364,6 @@ impl Token<'_> { ], instruction_data, &Program::token().into(), - Some(sender_mention), ) .await .map(|(resp, secrets)| { @@ -345,18 +382,30 @@ impl Token<'_> { amount: u128, holder_mention: &CliAccountMention, ) -> Result { - let account_ids = vec![definition_account_id, holder_account_id]; + let holder_identity = holder_mention.key_path().map_or( + AccountIdentity::Public(holder_account_id), + |key_path| AccountIdentity::PublicKeycard { + account_id: holder_account_id, + key_path: key_path.to_owned(), + }, + ); + + let program = Program::token(); let instruction = Instruction::Burn { amount_to_burn: amount, }; - - let mut groups = SigningGroup::new(); - groups - .add_required(holder_mention, holder_account_id, self.0) - .map_err(ExecutionFailureKind::from_anyhow)?; + let instruction_data = + Program::serialize_instruction(instruction).expect("Instruction should serialize"); self.0 - .send_public_tx(&Program::token(), account_ids, instruction, groups) + .send_pub_tx( + vec![ + AccountIdentity::PublicNoSign(definition_account_id), + holder_identity, + ], + instruction_data, + &program.into(), + ) .await } @@ -384,7 +433,6 @@ impl Token<'_> { ], instruction_data, &Program::token().into(), - None, ) .await .map(|(resp, secrets)| { @@ -413,11 +461,10 @@ impl Token<'_> { self.0 .resolve_private_account(definition_account_id) .ok_or(ExecutionFailureKind::KeyNotFoundError)?, - PrivacyPreservingAccount::Public(holder_account_id), + AccountIdentity::Public(holder_account_id), ], instruction_data, &Program::token().into(), - None, ) .await .map(|(resp, secrets)| { @@ -444,14 +491,13 @@ impl Token<'_> { self.0 .send_privacy_preserving_tx( vec![ - PrivacyPreservingAccount::Public(definition_account_id), + AccountIdentity::Public(definition_account_id), self.0 .resolve_private_account(holder_account_id) .ok_or(ExecutionFailureKind::KeyNotFoundError)?, ], instruction_data, &Program::token().into(), - None, ) .await .map(|(resp, secrets)| { @@ -471,19 +517,35 @@ impl Token<'_> { definition_mention: &CliAccountMention, holder_mention: &CliAccountMention, ) -> Result { - let account_ids = vec![definition_account_id, holder_account_id]; + let definition_identity = definition_mention.key_path().map_or( + AccountIdentity::Public(definition_account_id), + |key_path| AccountIdentity::PublicKeycard { + account_id: definition_account_id, + key_path: key_path.to_owned(), + }, + ); + + let holder_identity = holder_mention.key_path().map_or( + AccountIdentity::Public(holder_account_id), + |key_path| AccountIdentity::PublicKeycard { + account_id: holder_account_id, + key_path: key_path.to_owned(), + }, + ); + + let program = Program::token(); let instruction = Instruction::Mint { amount_to_mint: amount, }; - - let mut groups = SigningGroup::new(); - groups - .add_required(definition_mention, definition_account_id, self.0) - .and_then(|()| groups.add_optional(holder_mention, holder_account_id, self.0)) - .map_err(ExecutionFailureKind::from_anyhow)?; + let instruction_data = + Program::serialize_instruction(instruction).expect("Instruction should serialize"); self.0 - .send_public_tx(&Program::token(), account_ids, instruction, groups) + .send_pub_tx( + vec![definition_identity, holder_identity], + instruction_data, + &program.into(), + ) .await } @@ -511,7 +573,6 @@ impl Token<'_> { ], instruction_data, &Program::token().into(), - None, ) .await .map(|(resp, secrets)| { @@ -542,7 +603,7 @@ impl Token<'_> { self.0 .resolve_private_account(definition_account_id) .ok_or(ExecutionFailureKind::KeyNotFoundError)?, - PrivacyPreservingAccount::PrivateForeign { + AccountIdentity::PrivateForeign { npk: holder_npk, vpk: holder_vpk, identifier: holder_identifier, @@ -550,7 +611,6 @@ impl Token<'_> { ], instruction_data, &Program::token().into(), - None, ) .await .map(|(resp, secrets)| { @@ -579,11 +639,10 @@ impl Token<'_> { self.0 .resolve_private_account(definition_account_id) .ok_or(ExecutionFailureKind::KeyNotFoundError)?, - PrivacyPreservingAccount::Public(holder_account_id), + AccountIdentity::Public(holder_account_id), ], instruction_data, &Program::token().into(), - None, ) .await .map(|(resp, secrets)| { @@ -610,14 +669,13 @@ impl Token<'_> { self.0 .send_privacy_preserving_tx( vec![ - PrivacyPreservingAccount::Public(definition_account_id), + AccountIdentity::Public(definition_account_id), self.0 .resolve_private_account(holder_account_id) .ok_or(ExecutionFailureKind::KeyNotFoundError)?, ], instruction_data, &Program::token().into(), - None, ) .await .map(|(resp, secrets)| { @@ -646,8 +704,8 @@ impl Token<'_> { self.0 .send_privacy_preserving_tx( vec![ - PrivacyPreservingAccount::Public(definition_account_id), - PrivacyPreservingAccount::PrivateForeign { + AccountIdentity::Public(definition_account_id), + AccountIdentity::PrivateForeign { npk: holder_npk, vpk: holder_vpk, identifier: holder_identifier, @@ -655,7 +713,6 @@ impl Token<'_> { ], instruction_data, &Program::token().into(), - None, ) .await .map(|(resp, secrets)| { diff --git a/wallet/src/signing.rs b/wallet/src/signing.rs index 47dd9ec1..661bbdb0 100644 --- a/wallet/src/signing.rs +++ b/wallet/src/signing.rs @@ -1,119 +1,6 @@ -use anyhow::Result; use keycard_wallet::{KeycardWallet, python_path}; -use nssa::{AccountId, PrivateKey, PublicKey, Signature}; use pyo3::Python; -use crate::{WalletCore, cli::CliAccountMention}; - -/// Groups transaction signers by type to minimise Python GIL acquisition. -/// -/// Local signers are signed in pure Rust; all keycard signers share a single Python session -/// with one `connect` / `close_session` pair. -#[derive(Default)] -pub struct SigningGroup { - local: Vec<(AccountId, PrivateKey)>, - keycard: Vec<(AccountId, String)>, -} - -impl SigningGroup { - #[must_use] - pub fn new() -> Self { - Self::default() - } - - /// Add a sender. Keycard paths are queued for the hardware session; local accounts - /// have their signing key resolved eagerly. Errors if no key is found. - pub fn add_required( - &mut self, - mention: &CliAccountMention, - account_id: AccountId, - wallet_core: &WalletCore, - ) -> Result<()> { - if let CliAccountMention::KeyPath(path) = mention { - self.keycard.push((account_id, path.clone())); - return Ok(()); - } - let key = wallet_core - .storage() - .key_chain() - .pub_account_signing_key(account_id) - .ok_or_else(|| anyhow::anyhow!("signing key not found for account {account_id}"))? - .clone(); - self.local.push((account_id, key)); - Ok(()) - } - - /// Add a recipient. Same as [`add_required`] but silently skips accounts with no local - /// key and no keycard path — they are foreign and require neither a signature nor a nonce. - pub fn add_optional( - &mut self, - mention: &CliAccountMention, - account_id: AccountId, - wallet_core: &WalletCore, - ) -> Result<()> { - if let CliAccountMention::KeyPath(path) = mention { - self.keycard.push((account_id, path.clone())); - return Ok(()); - } - if let Some(key) = wallet_core - .storage() - .key_chain() - .pub_account_signing_key(account_id) - { - self.local.push((account_id, key.clone())); - } - Ok(()) - } - - /// Returns `true` when a PIN is required (at least one keycard signer is present). - #[must_use] - pub const fn needs_pin(&self) -> bool { - !self.keycard.is_empty() - } - - /// Account IDs that require a nonce (every non-foreign signer). - #[must_use] - pub fn signing_ids(&self) -> Vec { - self.local - .iter() - .map(|(id, _)| *id) - .chain(self.keycard.iter().map(|(id, _)| *id)) - .collect() - } - - /// Sign `hash` for every account in the group. - /// - /// Local accounts are signed in pure Rust. Keycard accounts share one Python session. - pub fn sign_all(&self, hash: &[u8; 32], pin: &str) -> Result> { - let mut sigs: Vec<(Signature, PublicKey)> = self - .local - .iter() - .map(|(_, key)| { - ( - Signature::new(key, hash), - PublicKey::new_from_private_key(key), - ) - }) - .collect(); - - if !self.keycard.is_empty() { - pyo3::Python::with_gil(|py| -> pyo3::PyResult<()> { - python_path::add_python_path(py)?; - let wallet = KeycardWallet::new(py)?; - wallet.connect(py, pin)?; - for (_, path) in &self.keycard { - sigs.push(wallet.sign_message_for_path(py, path, hash)?); - } - drop(wallet.close_session(py)); - Ok(()) - }) - .map_err(anyhow::Error::from)?; - } - - Ok(sigs) - } -} - /// Lazily opens and reuses a single Keycard session for all keycard signers in one transaction. pub struct KeycardSessionContext { pin: String, diff --git a/wallet/src/storage/key_chain.rs b/wallet/src/storage/key_chain.rs index e00dee8d..637884f1 100644 --- a/wallet/src/storage/key_chain.rs +++ b/wallet/src/storage/key_chain.rs @@ -55,7 +55,7 @@ pub struct SharedAccountEntry { /// 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). pub pda_seed: Option, - pub pda_program_id: Option, + pub authority_program_id: Option, pub account: Account, } @@ -858,7 +858,7 @@ mod tests { group_label: Label::new("test-group"), identifier: 42, pda_seed: None, - pda_program_id: None, + authority_program_id: None, account: nssa_core::account::Account::default(), }; let encoded = bincode::serialize(&entry).expect("serialize"); @@ -871,7 +871,7 @@ mod tests { group_label: Label::new("pda-group"), identifier: u128::MAX, pda_seed: Some(PdaSeed::new([7_u8; 32])), - pda_program_id: Some([9; 8]), + authority_program_id: Some([9; 8]), account: nssa_core::account::Account::default(), }; let pda_encoded = bincode::serialize(&pda_entry).expect("serialize pda"); @@ -890,7 +890,7 @@ mod tests { group_label: Label::new("old"), identifier: 1, pda_seed: None, - pda_program_id: None, + authority_program_id: None, account: nssa_core::account::Account::default(), }; let encoded = bincode::serialize(&entry).expect("serialize");