feat!(wallet): Merged SigningGroup with AccountManager (#500)

* feat: account manager extension

* feat(wallet): added unified way of sending public transactions to all facades

* fix(wallet): no sign option added

* fix(deny): deny fix

* fix(wallet): suggestion 1

* fix(wallet): suggestion fix 1

* feat!: Add new path for externally provided seed to the circuit.

BREAKING CHANGE: add identity variants to the circuit and change semantics for `Claim::Authorized` for private PDAs

* feat(ci): use separate job per each integration tests module

* feat(ci): cache rust artifacts

* feat(ci): build integration tests binary once and reuse it

* fix(wallet): fmt

* ci: add bench-regression workflow with criterion-compare for crypto_primitives_bench

* fix(wallet): merge postfix

* feat!(wallet): SigningGroup merged with AccountManager

* fix(ci): deny and artifacts fix

* fix(deny): deny fix

* fix keycard and lint

---------

Co-authored-by: Sergio Chouhy <sergio.chouhy@gmail.com>
Co-authored-by: Daniil Polyakov <arjentix@gmail.com>
Co-authored-by: Moudy <m.ellaz@hotmail.com>
Co-authored-by: Sergio Chouhy <41742639+schouhy@users.noreply.github.com>
Co-authored-by: jonesmarvin8 <83104039+jonesmarvin8@users.noreply.github.com>
This commit is contained in:
Pravdyvy 2026-05-28 00:34:08 +03:00 committed by GitHub
parent a927955e04
commit 8ada8ee2da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
77 changed files with 1251 additions and 834 deletions

View File

@ -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-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-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-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-0118", reason = "`hickory-proto` v0.25.0-alpha.5 is present transitively from logos crates, modification may break integration" },
{ id = "RUSTSEC-2026-0119", reason = "`hickory-proto` v0.25.0-alpha.5 is present transitively from logos crates, modification may break integration" }, { id = "RUSTSEC-2026-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" yanked = "deny"
unused-ignored-advisory = "deny" unused-ignored-advisory = "deny"

44
.github/workflows/bench-regression.yml vendored Normal file
View File

@ -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 }}

View File

@ -94,6 +94,12 @@ jobs:
- name: Install active toolchain - name: Install active toolchain
run: rustup install 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 - name: Lint workspace
env: env:
RISC0_SKIP_BUILD: "1" RISC0_SKIP_BUILD: "1"
@ -123,6 +129,12 @@ jobs:
- name: Install active toolchain - name: Install active toolchain
run: rustup install 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 - name: Install nextest
run: cargo install --locked cargo-nextest run: cargo install --locked cargo-nextest
@ -132,9 +144,10 @@ jobs:
RUST_LOG: "info" RUST_LOG: "info"
run: cargo nextest run --workspace --exclude integration_tests --all-features run: cargo nextest run --workspace --exclude integration_tests --all-features
integration-tests: integration-tests-prebuild:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 90 # TODO: Apply CI cache to speed this up outputs:
targets: ${{ steps.discover-targets.outputs.targets }}
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
@ -151,6 +164,75 @@ jobs:
- name: Install active toolchain - name: Install active toolchain
run: rustup install 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 - name: Install nextest
run: cargo install --locked cargo-nextest run: cargo install --locked cargo-nextest
@ -158,7 +240,7 @@ jobs:
env: env:
RISC0_DEV_MODE: "1" RISC0_DEV_MODE: "1"
RUST_LOG: "info" 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: valid-proof-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -179,6 +261,12 @@ jobs:
- name: Install active toolchain - name: Install active toolchain
run: rustup install 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 - name: Test valid proof
env: env:
RUST_LOG: "info" RUST_LOG: "info"
@ -196,6 +284,12 @@ jobs:
- uses: ./.github/actions/install-risc0 - 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 - name: Install just
run: cargo install --locked just run: cargo install --locked just

8
Cargo.lock generated
View File

@ -3646,7 +3646,7 @@ dependencies = [
"libc", "libc",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"socket2 0.6.3", "socket2 0.5.10",
"tokio", "tokio",
"tower-service", "tower-service",
"tracing", "tracing",
@ -7468,7 +7468,7 @@ dependencies = [
"quinn-udp", "quinn-udp",
"rustc-hash", "rustc-hash",
"rustls", "rustls",
"socket2 0.6.3", "socket2 0.5.10",
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio", "tokio",
"tracing", "tracing",
@ -7505,7 +7505,7 @@ dependencies = [
"cfg_aliases", "cfg_aliases",
"libc", "libc",
"once_cell", "once_cell",
"socket2 0.6.3", "socket2 0.5.10",
"tracing", "tracing",
"windows-sys 0.60.2", "windows-sys 0.60.2",
] ]
@ -8471,7 +8471,7 @@ dependencies = [
"security-framework", "security-framework",
"security-framework-sys", "security-framework-sys",
"webpki-root-certs 0.26.11", "webpki-root-certs 0.26.11",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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: Luckily all that complexity is hidden behind the `wallet_core.send_privacy_preserving_tx` function:
```rust ```rust
let accounts = vec![PrivacyPreservingAccount::PrivateOwned(account_id)]; let accounts = vec![AccountIdentity::PrivateOwned(account_id)];
// Construct and submit the privacy-preserving transaction // Construct and submit the privacy-preserving transaction
wallet_core wallet_core

View File

@ -1,5 +1,5 @@
use nssa::{AccountId, program::Program}; 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: // 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 // Define the desired greeting in ASCII
let greeting: Vec<u8> = vec![72, 111, 108, 97, 32, 109, 117, 110, 100, 111, 33]; let greeting: Vec<u8> = 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 // Construct and submit the privacy-preserving transaction
wallet_core wallet_core
@ -52,7 +52,6 @@ async fn main() {
accounts, accounts,
Program::serialize_instruction(greeting).unwrap(), Program::serialize_instruction(greeting).unwrap(),
&program.into(), &program.into(),
None,
) )
.await .await
.unwrap(); .unwrap();

View File

@ -4,7 +4,7 @@ use nssa::{
AccountId, ProgramId, privacy_preserving_transaction::circuit::ProgramWithDependencies, AccountId, ProgramId, privacy_preserving_transaction::circuit::ProgramWithDependencies,
program::Program, program::Program,
}; };
use wallet::{PrivacyPreservingAccount, WalletCore}; use wallet::{AccountIdentity, WalletCore};
// Before running this example, compile the `simple_tail_call.rs` guest program with: // 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(); std::iter::once((hello_world.id(), hello_world)).collect();
let program_with_dependencies = ProgramWithDependencies::new(simple_tail_call, dependencies); 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 // Construct and submit the privacy-preserving transaction
let instruction = (); let instruction = ();
@ -60,7 +60,6 @@ async fn main() {
accounts, accounts,
Program::serialize_instruction(instruction).unwrap(), Program::serialize_instruction(instruction).unwrap(),
&program_with_dependencies, &program_with_dependencies,
None,
) )
.await .await
.unwrap(); .unwrap();

View File

@ -2,7 +2,7 @@ use clap::{Parser, Subcommand};
use common::transaction::NSSATransaction; use common::transaction::NSSATransaction;
use nssa::{PublicTransaction, program::Program, public_transaction}; use nssa::{PublicTransaction, program::Program, public_transaction};
use sequencer_service_rpc::RpcClient as _; 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: // 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 instruction: Instruction = (WRITE_FUNCTION_ID, greeting.into_bytes());
let account_id = account_id.parse().unwrap(); let account_id = account_id.parse().unwrap();
let accounts = vec![PrivacyPreservingAccount::PrivateOwned(account_id)]; let accounts = vec![AccountIdentity::PrivateOwned(account_id)];
wallet_core wallet_core
.send_privacy_preserving_tx( .send_privacy_preserving_tx(
accounts, accounts,
Program::serialize_instruction(instruction).unwrap(), Program::serialize_instruction(instruction).unwrap(),
&program.into(), &program.into(),
None,
) )
.await .await
.unwrap(); .unwrap();
@ -139,8 +138,8 @@ async fn main() {
let to = to.parse().unwrap(); let to = to.parse().unwrap();
let accounts = vec![ let accounts = vec![
PrivacyPreservingAccount::Public(from), AccountIdentity::Public(from),
PrivacyPreservingAccount::PrivateOwned(to), AccountIdentity::PrivateOwned(to),
]; ];
wallet_core wallet_core
@ -148,7 +147,6 @@ async fn main() {
accounts, accounts,
Program::serialize_instruction(instruction).unwrap(), Program::serialize_instruction(instruction).unwrap(),
&program.into(), &program.into(),
None,
) )
.await .await
.unwrap(); .unwrap();

View File

@ -704,6 +704,7 @@ async fn ppt_that_chain_calls_faucet_is_dropped() -> Result<()> {
npk, npk,
ssk, ssk,
identifier: 1337, identifier: 1337,
seed: None,
}, },
], ],
&program_with_deps, &program_with_deps,

View File

@ -6,27 +6,37 @@
use std::{path::PathBuf, time::Duration}; use std::{path::PathBuf, time::Duration};
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use authenticated_transfer_core::Instruction as AuthTransferInstruction;
use common::transaction::NSSATransaction;
use integration_tests::{ 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, verify_commitment_is_in_state,
}; };
use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder;
use log::info; use log::info;
use nssa::{ 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, 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 tokio::test;
use wallet::{ use wallet::{
PrivacyPreservingAccount, WalletCore, AccountIdentity, WalletCore,
cli::{Command, account::AccountSubcommand}, cli::{Command, account::AccountSubcommand},
}; };
/// Funds a private PDA via the proxy program with a chained call to `auth_transfer`. /// Funds a private PDA by calling `auth_transfer` directly.
///
/// 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`.
#[expect( #[expect(
clippy::too_many_arguments, clippy::too_many_arguments,
reason = "test helper — grouping args would obscure intent" reason = "test helper — grouping args would obscure intent"
@ -34,33 +44,68 @@ use wallet::{
async fn fund_private_pda( async fn fund_private_pda(
wallet: &WalletCore, wallet: &WalletCore,
sender: AccountId, sender: AccountId,
pda_account_id: AccountId,
npk: NullifierPublicKey, npk: NullifierPublicKey,
vpk: ViewingPublicKey, vpk: ViewingPublicKey,
identifier: u128, identifier: u128,
seed: PdaSeed, seed: PdaSeed,
authority_program_id: ProgramId,
amount: u128, amount: u128,
proxy_program: &ProgramWithDependencies, auth_transfer: &ProgramWithDependencies,
auth_transfer_id: ProgramId,
) -> Result<()> { ) -> Result<()> {
wallet let pda_account_id = AccountId::for_private_pda(&authority_program_id, &seed, &npk, identifier);
.send_privacy_preserving_tx( let sender_account = wallet
vec![ .get_account_public(sender)
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,
)
.await .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(()) Ok(())
} }
@ -79,22 +124,21 @@ async fn spend_private_pda(
seed: PdaSeed, seed: PdaSeed,
amount: u128, amount: u128,
spend_program: &ProgramWithDependencies, spend_program: &ProgramWithDependencies,
auth_transfer_id: nssa::ProgramId, auth_transfer_id: ProgramId,
) -> Result<()> { ) -> Result<()> {
wallet wallet
.send_privacy_preserving_tx( .send_privacy_preserving_tx(
vec![ vec![
PrivacyPreservingAccount::PrivatePdaOwned(pda_account_id), AccountIdentity::PrivatePdaOwned(pda_account_id),
PrivacyPreservingAccount::PrivateForeign { AccountIdentity::PrivateForeign {
npk: recipient_npk, npk: recipient_npk,
vpk: recipient_vpk, vpk: recipient_vpk,
identifier: 0, identifier: 0,
}, },
], ],
Program::serialize_instruction((seed, amount, auth_transfer_id, false)) Program::serialize_instruction((seed, amount, auth_transfer_id))
.context("failed to serialize pda_fund_spend_proxy instruction")?, .context("failed to serialize pda_spend_proxy instruction")?,
spend_program, spend_program,
None,
) )
.await .await
.map_err(|e| anyhow::anyhow!("{e}"))?; .map_err(|e| anyhow::anyhow!("{e}"))?;
@ -126,9 +170,9 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> {
let proxy = { let proxy = {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../artifacts/test_program_methods") .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:?}"))?) 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 auth_transfer = Program::authenticated_transfer_program();
let proxy_id = proxy.id(); 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 seed = PdaSeed::new([42; 32]);
let amount: u128 = 100; let amount: u128 = 100;
let auth_transfer_program = ProgramWithDependencies::new(auth_transfer.clone(), [].into());
let spend_program = let spend_program =
ProgramWithDependencies::new(proxy, [(auth_transfer_id, auth_transfer)].into()); 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( fund_private_pda(
ctx.wallet(), ctx.wallet(),
sender_0, sender_0,
alice_pda_0_id,
alice_npk, alice_npk,
alice_vpk.clone(), alice_vpk.clone(),
0, 0,
seed, seed,
proxy_id,
amount, amount,
&spend_program, &auth_transfer_program,
auth_transfer_id,
) )
.await?; .await?;
@ -168,14 +212,13 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> {
fund_private_pda( fund_private_pda(
ctx.wallet(), ctx.wallet(),
sender_1, sender_1,
alice_pda_1_id,
alice_npk, alice_npk,
alice_vpk.clone(), alice_vpk.clone(),
1, 1,
seed, seed,
proxy_id,
amount, amount,
&spend_program, &auth_transfer_program,
auth_transfer_id,
) )
.await?; .await?;

View File

@ -5,7 +5,7 @@ use crate::{
NullifierSecretKey, SharedSecretKey, NullifierSecretKey, SharedSecretKey,
account::{Account, AccountWithMetadata}, account::{Account, AccountWithMetadata},
encryption::Ciphertext, encryption::Ciphertext,
program::{BlockValidityWindow, ProgramId, ProgramOutput, TimestampValidityWindow}, program::{BlockValidityWindow, PdaSeed, ProgramId, ProgramOutput, TimestampValidityWindow},
}; };
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -60,15 +60,28 @@ pub enum InputAccountIdentity {
npk: NullifierPublicKey, npk: NullifierPublicKey,
ssk: SharedSecretKey, ssk: SharedSecretKey,
identifier: Identifier, 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 /// Update of an existing private PDA, with membership proof. `npk` is derived
/// from `nsk`. Authorization is established upstream by a caller `pda_seeds` match or a /// from `nsk`. Authorization may be established upstream by a caller `pda_seeds` match or a
/// previously-seen authorization in a chained call. /// previously-seen authorization in a chained call.
PrivatePdaUpdate { PrivatePdaUpdate {
ssk: SharedSecretKey, ssk: SharedSecretKey,
nsk: NullifierSecretKey, nsk: NullifierSecretKey,
membership_proof: MembershipProof, membership_proof: MembershipProof,
identifier: Identifier, 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)>,
}, },
} }

View File

@ -461,6 +461,7 @@ mod tests {
npk, npk,
ssk: shared_secret, ssk: shared_secret,
identifier, identifier,
seed: None,
}], }],
&program.clone().into(), &program.clone().into(),
) )
@ -488,7 +489,7 @@ mod tests {
let seed = PdaSeed::new([42; 32]); let seed = PdaSeed::new([42; 32]);
let shared_secret_pda = SharedSecretKey::new([55; 32], &keys.vpk()); let shared_secret_pda = SharedSecretKey::new([55; 32], &keys.vpk());
// PDA (new, mask 3) // PDA (new, private PDA)
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 0); let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 0);
let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id); let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id);
@ -506,6 +507,7 @@ mod tests {
npk, npk,
ssk: shared_secret_pda, ssk: shared_secret_pda,
identifier: 0, identifier: 0,
seed: None,
}], }],
&program_with_deps, &program_with_deps,
); );
@ -557,6 +559,7 @@ mod tests {
npk, npk,
ssk: shared_secret_pda, ssk: shared_secret_pda,
identifier: 0, identifier: 0,
seed: None,
}, },
InputAccountIdentity::Public, InputAccountIdentity::Public,
], ],
@ -747,7 +750,7 @@ mod tests {
/// to `PrivateAccountKind::Pda` carrying the correct `(program_id, seed, identifier)`. /// to `PrivateAccountKind::Pda` carrying the correct `(program_id, seed, identifier)`.
#[test] #[test]
fn private_pda_update_encrypts_pda_kind_with_identifier() { 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 auth_transfer = Program::authenticated_transfer_program();
let keys = test_private_account_keys_1(); let keys = test_private_account_keys_1();
let npk = keys.npk(); let npk = keys.npk();
@ -784,6 +787,7 @@ mod tests {
nsk: keys.nsk, nsk: keys.nsk,
membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(), membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(),
identifier, identifier,
seed: None,
}, },
InputAccountIdentity::Public, InputAccountIdentity::Public,
], ],
@ -819,6 +823,7 @@ mod tests {
npk, npk,
ssk: shared_secret, ssk: shared_secret,
identifier: 99, identifier: 99,
seed: None,
}], }],
&program.into(), &program.into(),
); );
@ -828,7 +833,7 @@ mod tests {
#[test] #[test]
fn private_pda_update_identifier_mismatch_fails() { 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 auth_transfer = Program::authenticated_transfer_program();
let keys = test_private_account_keys_1(); let keys = test_private_account_keys_1();
let npk = keys.npk(); let npk = keys.npk();
@ -862,6 +867,7 @@ mod tests {
nsk: keys.nsk, nsk: keys.nsk,
membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(), membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(),
identifier: 99, identifier: 99,
seed: None,
}, },
InputAccountIdentity::Public, InputAccountIdentity::Public,
], ],

View File

@ -350,12 +350,12 @@ mod tests {
} }
#[must_use] #[must_use]
pub fn pda_fund_spend_proxy() -> Self { pub fn pda_spend_proxy() -> Self {
use test_program_methods::{PDA_FUND_SPEND_PROXY_ELF, PDA_FUND_SPEND_PROXY_ID}; use test_program_methods::{PDA_SPEND_PROXY_ELF, PDA_SPEND_PROXY_ID};
Self { Self {
id: PDA_FUND_SPEND_PROXY_ID, id: PDA_SPEND_PROXY_ID,
elf: PDA_FUND_SPEND_PROXY_ELF.to_vec(), elf: PDA_SPEND_PROXY_ELF.to_vec(),
} }
} }

View File

@ -2218,7 +2218,7 @@ pub mod tests {
assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); 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`, /// `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 /// so the circuit must reject. Here `simple_balance_transfer` emits no claim for the
/// second account, leaving position 1 unbound. /// second account, leaving position 1 unbound.
@ -2249,6 +2249,7 @@ pub mod tests {
npk, npk,
ssk: shared_secret, ssk: shared_secret,
identifier: u128::MAX, identifier: u128::MAX,
seed: None,
}, },
], ],
&program.into(), &program.into(),
@ -2257,7 +2258,7 @@ pub mod tests {
assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); 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 /// 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 /// 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 /// asserts it equals the `pre_state`'s `account_id`. The equality both validates the claim
@ -2280,11 +2281,12 @@ pub mod tests {
npk, npk,
ssk: shared_secret, ssk: shared_secret,
identifier: u128::MAX, identifier: u128::MAX,
seed: None,
}], }],
&program.into(), &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_nullifiers.len(), 1);
assert_eq!(output.new_commitments.len(), 1); assert_eq!(output.new_commitments.len(), 1);
assert_eq!(output.ciphertexts.len(), 1); assert_eq!(output.ciphertexts.len(), 1);
@ -2319,6 +2321,7 @@ pub mod tests {
npk: npk_b, npk: npk_b,
ssk: shared_secret, ssk: shared_secret,
identifier: u128::MAX, identifier: u128::MAX,
seed: None,
}], }],
&program.into(), &program.into(),
); );
@ -2326,7 +2329,7 @@ pub mod tests {
assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); 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 /// 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 /// seed via `ChainedCall.pda_seeds`. In the callee's step, the `pre_state`'s authorization
/// is established via the private derivation /// is established via the private derivation
@ -2354,12 +2357,13 @@ pub mod tests {
npk, npk,
ssk: shared_secret, ssk: shared_secret,
identifier: u128::MAX, identifier: u128::MAX,
seed: None,
}], }],
&program_with_deps, &program_with_deps,
); );
let (output, _proof) = 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_commitments.len(), 1);
assert_eq!(output.new_nullifiers.len(), 1); assert_eq!(output.new_nullifiers.len(), 1);
} }
@ -2392,6 +2396,7 @@ pub mod tests {
npk, npk,
ssk: shared_secret, ssk: shared_secret,
identifier: u128::MAX, identifier: u128::MAX,
seed: None,
}], }],
&program_with_deps, &program_with_deps,
); );
@ -2401,8 +2406,8 @@ pub mod tests {
/// Exploit-scenario pin. A single `(program_id, seed)` pair can derive a family of /// 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 /// `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 /// family-binding check, a program could claim `PDA_alice` (`alice_npk`) and
/// `PDA_bob` (mask-3, `bob_npk`) under the same seed in one transaction, and once reuse /// `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 /// 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 /// `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 /// here: after the first claim records `(program, seed) → PDA_alice`, the second claim
@ -2430,11 +2435,13 @@ pub mod tests {
npk: keys_a.npk(), npk: keys_a.npk(),
ssk: shared_a, ssk: shared_a,
identifier: u128::MAX, identifier: u128::MAX,
seed: None,
}, },
InputAccountIdentity::PrivatePdaInit { InputAccountIdentity::PrivatePdaInit {
npk: keys_b.npk(), npk: keys_b.npk(),
ssk: shared_b, ssk: shared_b,
identifier: u128::MAX, identifier: u128::MAX,
seed: None,
}, },
], ],
&program.into(), &program.into(),
@ -2443,17 +2450,11 @@ pub mod tests {
assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
} }
/// Pins the current limitation: a mask-3 PDA that was claimed in a previous transaction /// A private PDA that is reused at top level without an external seed in the identity still
/// cannot be re-used in a new transaction as-is. This PR only binds supplied npks via a /// fails binding. The noop program emits no `Claim::Pda` and there is no caller
/// 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
/// `ChainedCall.pda_seeds`, so position 0 is never bound and the assertion fires. /// `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 /// Supplying `seed: Some((seed, owner_program_id))` in the `PrivatePdaUpdate` identity is
// `(seed, original_owner_program_id)` side input per mask-3 `pre_state` so the circuit /// the correct path for top-level reuse; this test pins the failure when no seed is provided.
// can re-verify `AccountId::for_private_pda(owner, seed, npk) == pre.account_id` without a
// claim.
#[test] #[test]
fn private_pda_top_level_reuse_rejected_by_binding_check() { fn private_pda_top_level_reuse_rejected_by_binding_check() {
let program = Program::noop(); let program = Program::noop();
@ -2481,6 +2482,7 @@ pub mod tests {
npk, npk,
ssk: shared_secret, ssk: shared_secret,
identifier: u128::MAX, identifier: u128::MAX,
seed: None,
}], }],
&program.into(), &program.into(),
); );
@ -4372,15 +4374,15 @@ pub mod tests {
let alice_keys = test_private_account_keys_1(); let alice_keys = test_private_account_keys_1();
let alice_npk = alice_keys.npk(); 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 auth_transfer = Program::authenticated_transfer_program();
let proxy_id = proxy.id(); let proxy_id = proxy.id();
let auth_transfer_id = auth_transfer.id(); let auth_transfer_id = auth_transfer.id();
let seed = PdaSeed::new([42; 32]); let seed = PdaSeed::new([42; 32]);
let amount: u128 = 100; let amount: u128 = 100;
let program_with_deps = let spend_with_deps =
ProgramWithDependencies::new(proxy, [(auth_transfer_id, auth_transfer)].into()); ProgramWithDependencies::new(proxy, [(auth_transfer_id, auth_transfer.clone())].into());
let funder_id = funder_keys.account_id(); let funder_id = funder_keys.account_id();
let alice_pda_0_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, 0); 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_0 = SharedSecretKey::new([10; 32], &alice_keys.vpk());
let alice_shared_1 = SharedSecretKey::new([11; 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_account = state.get_account_by_id(funder_id);
let funder_nonce = funder_account.nonce; let funder_nonce = funder_account.nonce;
@ -4415,16 +4417,18 @@ pub mod tests {
AccountWithMetadata::new(funder_account, true, funder_id), AccountWithMetadata::new(funder_account, true, funder_id),
AccountWithMetadata::new(Account::default(), false, alice_pda_0_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![ vec![
InputAccountIdentity::Public, InputAccountIdentity::Public,
InputAccountIdentity::PrivatePdaInit { InputAccountIdentity::PrivatePdaInit {
npk: alice_npk, npk: alice_npk,
ssk: alice_shared_0, ssk: alice_shared_0,
identifier: 0, identifier: 0,
seed: Some((seed, proxy_id)),
}, },
], ],
&program_with_deps, &auth_transfer.clone().into(),
) )
.unwrap(); .unwrap();
let message = Message::try_from_circuit_output( let message = Message::try_from_circuit_output(
@ -4448,7 +4452,7 @@ pub mod tests {
.unwrap(); .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_account = state.get_account_by_id(funder_id);
let funder_nonce = funder_account.nonce; let funder_nonce = funder_account.nonce;
@ -4457,16 +4461,18 @@ pub mod tests {
AccountWithMetadata::new(funder_account, true, funder_id), AccountWithMetadata::new(funder_account, true, funder_id),
AccountWithMetadata::new(Account::default(), false, alice_pda_1_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![ vec![
InputAccountIdentity::Public, InputAccountIdentity::Public,
InputAccountIdentity::PrivatePdaInit { InputAccountIdentity::PrivatePdaInit {
npk: alice_npk, npk: alice_npk,
ssk: alice_shared_1, ssk: alice_shared_1,
identifier: 1, identifier: 1,
seed: Some((seed, proxy_id)),
}, },
], ],
&program_with_deps, &auth_transfer.into(),
) )
.unwrap(); .unwrap();
let message = Message::try_from_circuit_output( 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(alice_pda_0_account, true, alice_pda_0_id),
AccountWithMetadata::new(recipient_account, true, recipient_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![ vec![
InputAccountIdentity::PrivatePdaUpdate { InputAccountIdentity::PrivatePdaUpdate {
ssk: alice_shared_0, ssk: alice_shared_0,
@ -4513,10 +4519,11 @@ pub mod tests {
.get_proof_for_commitment(&commitment_pda_0) .get_proof_for_commitment(&commitment_pda_0)
.expect("pda_0 must be in state"), .expect("pda_0 must be in state"),
identifier: 0, identifier: 0,
seed: None,
}, },
InputAccountIdentity::Public, InputAccountIdentity::Public,
], ],
&program_with_deps, &spend_with_deps,
) )
.unwrap(); .unwrap();
let message = Message::try_from_circuit_output( 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 recipient_account = state.get_account_by_id(recipient_id);
let (output, proof) = execute_and_prove( let (output, proof) = execute_and_prove(
vec![ 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), 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![ vec![
InputAccountIdentity::PrivatePdaUpdate { InputAccountIdentity::PrivatePdaUpdate {
ssk: alice_shared_1, ssk: alice_shared_1,
@ -4557,10 +4564,11 @@ pub mod tests {
.get_proof_for_commitment(&commitment_pda_1) .get_proof_for_commitment(&commitment_pda_1)
.expect("pda_1 must be in state"), .expect("pda_1 must be in state"),
identifier: 1, identifier: 1,
seed: None,
}, },
InputAccountIdentity::Public, InputAccountIdentity::Public,
], ],
&program_with_deps, &spend_with_deps,
) )
.unwrap(); .unwrap();
let message = Message::try_from_circuit_output( 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); 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);
} }
} }

View File

@ -305,6 +305,68 @@ impl ExecutionState {
} }
Entry::Vacant(_) => { Entry::Vacant(_) => {
// Pre state for the initial call // 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); 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 { match claim {
Claim::Authorized => { Claim::Authorized => {}
assert!(
pre_is_authorized,
"Cannot claim unauthorized private PDA {pre_account_id}"
);
}
Claim::Pda(seed) => { Claim::Pda(seed) => {
let (npk, identifier) = self let (npk, identifier) = self
.private_pda_npk_by_position .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; post.account_mut().program_owner = program_id;

View File

@ -148,6 +148,7 @@ pub fn compute_circuit_output(
npk: _, npk: _,
ssk, ssk,
identifier, identifier,
seed: _,
} => { } => {
// The npk-to-account_id binding is established upstream in // The npk-to-account_id binding is established upstream in
// `validate_and_sync_states` via `Claim::Pda(seed)` or a caller `pda_seeds` // `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 new_nonce = Nonce::private_account_nonce_init(&pre_state.account_id);
let account_id = 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) .get(&pos)
.expect("PrivatePdaInit position must be in pda_seed_by_position"); .expect("PrivatePdaInit position must be in pda_seed_by_position");
emit_private_output( emit_private_output(
@ -181,7 +182,7 @@ pub fn compute_circuit_output(
post_state, post_state,
&account_id, &account_id,
&PrivateAccountKind::Pda { &PrivateAccountKind::Pda {
program_id: *pda_program_id, program_id: *authority_program_id,
seed: *seed, seed: *seed,
identifier: *identifier, identifier: *identifier,
}, },
@ -195,14 +196,16 @@ pub fn compute_circuit_output(
nsk, nsk,
membership_proof, membership_proof,
identifier, identifier,
seed: external_seed,
} => { } => {
// The npk binding is established upstream. Authorization must already be set; // With an external seed the binding comes from the circuit input and the
// an unauthorized PrivatePdaUpdate would mean the prover supplied an nsk for an // pre_state is intentionally unauthorized; without one the binding comes from
// unbound PDA, which the upstream binding check would have rejected anyway, // a Claim or caller pda_seeds, so the pre_state must already be authorized.
// but we assert here to fail fast and document the precondition. // When `external_seed` is `Some`, execution_state already asserted
// `!pre_state.is_authorized`.
assert!( assert!(
pre_state.is_authorized, pre_state.is_authorized ^ external_seed.is_some(),
"PrivatePdaUpdate requires authorized pre_state" "PrivatePdaUpdate requires authorized pre_state or external seed"
); );
let new_nullifier = compute_update_nullifier_and_set_digest( 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 new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk);
let account_id = 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) .get(&pos)
.expect("PrivatePdaUpdate position must be in pda_seed_by_position"); .expect("PrivatePdaUpdate position must be in pda_seed_by_position");
emit_private_output( emit_private_output(
@ -223,7 +226,7 @@ pub fn compute_circuit_output(
post_state, post_state,
&account_id, &account_id,
&PrivateAccountKind::Pda { &PrivateAccountKind::Pda {
program_id: *pda_program_id, program_id: *authority_program_id,
seed: *seed, seed: *seed,
identifier: *identifier, identifier: *identifier,
}, },

View File

@ -34,7 +34,7 @@ pub mod setup;
pub const TIME_TO_WAIT_FOR_BLOCK_SECONDS: u64 = 12; 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_DATA_CHANGER: &str = "data_changer.bin";
pub const NSSA_PROGRAM_FOR_TEST_NOOP: &str = "noop.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_WITH_OPEN_PORT: &str = "logos-blockchain-node-0";
pub(crate) const BEDROCK_SERVICE_PORT: u16 = 18080; pub(crate) const BEDROCK_SERVICE_PORT: u16 = 18080;

View File

@ -9,9 +9,7 @@ use sequencer_service::{GenesisAction, SequencerHandle};
use sequencer_service_rpc::RpcClient as _; use sequencer_service_rpc::RpcClient as _;
use tempfile::TempDir; use tempfile::TempDir;
use testcontainers::compose::DockerCompose; use testcontainers::compose::DockerCompose;
use wallet::{ use wallet::{AccDecodeData::Decode, AccountIdentity, WalletCore, config::WalletConfigOverrides};
AccDecodeData::Decode, PrivacyPreservingAccount, WalletCore, config::WalletConfigOverrides,
};
use crate::{ use crate::{
BEDROCK_SERVICE_PORT, BEDROCK_SERVICE_WITH_OPEN_PORT, 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 let (tx_hash, mut secrets) = wallet
.send_privacy_preserving_tx( .send_privacy_preserving_tx(
vec![ vec![
PrivacyPreservingAccount::PrivateOwned(owner_id), AccountIdentity::PrivateOwned(owner_id),
PrivacyPreservingAccount::Public(owner_vault_id), AccountIdentity::Public(owner_vault_id),
], ],
instruction_data, instruction_data,
&program_with_dependencies, &program_with_dependencies,
None,
) )
.await .await
.context("Failed to submit private vault claim transaction")?; .context("Failed to submit private vault claim transaction")?;

View File

@ -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::<Instruction>();
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();
}

View File

@ -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::<Instruction>();
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();
}

View File

@ -15,7 +15,7 @@ use test_fixtures::{DiskSizes, TestContext};
use wallet::cli::SubcommandReturnValue; use wallet::cli::SubcommandReturnValue;
const TX_INCLUSION_POLL_INTERVAL: Duration = Duration::from_millis(250); 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` /// 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 /// is the full Block (header + body + bedrock metadata) and is the closest

View File

@ -181,7 +181,7 @@ async fn measure_bedrock_finality(ctx: &TestContext) -> Result<Duration> {
.context("connect indexer WS")?; .context("connect indexer WS")?;
let sequencer_tip = ctx.sequencer_client().get_last_block_id().await?; 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 started = std::time::Instant::now();
let poll = async { let poll = async {
loop { loop {

View File

@ -1,6 +1,7 @@
use anyhow::Result; use anyhow::Result;
use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder; 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::{ use nssa_core::{
Identifier, InputAccountIdentity, MembershipProof, NullifierPublicKey, NullifierSecretKey, Identifier, InputAccountIdentity, MembershipProof, NullifierPublicKey, NullifierSecretKey,
SharedSecretKey, SharedSecretKey,
@ -11,8 +12,15 @@ use nssa_core::{
use crate::{ExecutionFailureKind, WalletCore}; use crate::{ExecutionFailureKind, WalletCore};
#[derive(Clone)] #[derive(Clone)]
pub enum PrivacyPreservingAccount { pub enum AccountIdentity {
Public(AccountId), 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), PrivateOwned(AccountId),
PrivateForeign { PrivateForeign {
npk: NullifierPublicKey, npk: NullifierPublicKey,
@ -50,10 +58,15 @@ pub enum PrivacyPreservingAccount {
}, },
} }
impl PrivacyPreservingAccount { impl AccountIdentity {
#[must_use] #[must_use]
/// Note: `PublicNoSign` still counts as public, the variant just suppresses the signing-key
/// lookup.
pub const fn is_public(&self) -> bool { pub const fn is_public(&self) -> bool {
matches!(&self, Self::Public(_)) matches!(
&self,
Self::Public(_) | Self::PublicNoSign(_) | Self::PublicKeycard { .. }
)
} }
#[must_use] #[must_use]
@ -82,23 +95,29 @@ enum State {
account: AccountWithMetadata, account: AccountWithMetadata,
sk: Option<PrivateKey>, sk: Option<PrivateKey>,
}, },
PublicKeycard {
account: AccountWithMetadata,
key_path: String,
},
Private(AccountPreparedData), Private(AccountPreparedData),
} }
pub struct AccountManager { pub struct AccountManager {
states: Vec<State>, states: Vec<State>,
pin: Option<String>,
} }
impl AccountManager { impl AccountManager {
pub async fn new( pub async fn new(
wallet: &WalletCore, wallet: &WalletCore,
accounts: Vec<PrivacyPreservingAccount>, accounts: Vec<AccountIdentity>,
) -> Result<Self, ExecutionFailureKind> { ) -> Result<Self, ExecutionFailureKind> {
let mut states = Vec::with_capacity(accounts.len()); let mut states = Vec::with_capacity(accounts.len());
let mut pin = None;
for account in accounts { for account in accounts {
let state = match account { let state = match account {
PrivacyPreservingAccount::Public(account_id) => { AccountIdentity::Public(account_id) => {
let acc = wallet let acc = wallet
.get_account_public(account_id) .get_account_public(account_id)
.await .await
@ -109,12 +128,52 @@ impl AccountManager {
State::Public { account, sk } 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?; let pre = private_key_tree_acc_preparation(wallet, account_id, false).await?;
State::Private(pre) State::Private(pre)
} }
PrivacyPreservingAccount::PrivateForeign { AccountIdentity::PrivateForeign {
npk, npk,
vpk, vpk,
identifier, identifier,
@ -138,11 +197,11 @@ impl AccountManager {
State::Private(pre) State::Private(pre)
} }
PrivacyPreservingAccount::PrivatePdaOwned(account_id) => { AccountIdentity::PrivatePdaOwned(account_id) => {
let pre = private_key_tree_acc_preparation(wallet, account_id, true).await?; let pre = private_key_tree_acc_preparation(wallet, account_id, true).await?;
State::Private(pre) State::Private(pre)
} }
PrivacyPreservingAccount::PrivatePdaForeign { AccountIdentity::PrivatePdaForeign {
account_id, account_id,
npk, npk,
vpk, vpk,
@ -166,7 +225,7 @@ impl AccountManager {
}; };
State::Private(pre) State::Private(pre)
} }
PrivacyPreservingAccount::PrivateShared { AccountIdentity::PrivateShared {
nsk, nsk,
npk, npk,
vpk, vpk,
@ -180,7 +239,7 @@ impl AccountManager {
State::Private(pre) State::Private(pre)
} }
PrivacyPreservingAccount::PrivatePdaShared { AccountIdentity::PrivatePdaShared {
account_id, account_id,
nsk, nsk,
npk, npk,
@ -199,27 +258,33 @@ impl AccountManager {
states.push(state); states.push(state);
} }
Ok(Self { states }) Ok(Self { states, pin })
} }
pub fn pre_states(&self) -> Vec<AccountWithMetadata> { pub fn pre_states(&self) -> Vec<AccountWithMetadata> {
self.states self.states
.iter() .iter()
.map(|state| match state { .map(|state| match state {
State::Public { account, .. } => account.clone(), State::Public { account, .. } | State::PublicKeycard { account, .. } => {
account.clone()
}
State::Private(pre) => pre.pre_state.clone(), State::Private(pre) => pre.pre_state.clone(),
}) })
.collect() .collect()
} }
pub fn public_account_nonces(&self) -> Vec<Nonce> { pub fn public_account_nonces(&self) -> Vec<Nonce> {
self.states // Must match the signature order produced by sign_message(): local accounts first,
.iter() // keycard accounts second.
.filter_map(|state| match state { let local = self.states.iter().filter_map(|state| match state {
State::Public { account, sk } => sk.as_ref().map(|_| account.account.nonce), State::Public { account, sk } => sk.as_ref().map(|_| account.account.nonce),
State::Private(_) => None, State::PublicKeycard { .. } | State::Private(_) => None,
}) });
.collect() 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<PrivateAccountKeys> { pub fn private_account_keys(&self) -> Vec<PrivateAccountKeys> {
@ -232,7 +297,7 @@ impl AccountManager {
vpk: pre.vpk.clone(), vpk: pre.vpk.clone(),
epk: pre.epk.clone(), epk: pre.epk.clone(),
}), }),
State::Public { .. } => None, State::Public { .. } | State::PublicKeycard { .. } => None,
}) })
.collect() .collect()
} }
@ -245,18 +310,20 @@ impl AccountManager {
self.states self.states
.iter() .iter()
.map(|state| match state { .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()) { State::Private(pre) if pre.is_pda => match (pre.nsk, pre.proof.clone()) {
(Some(nsk), Some(membership_proof)) => InputAccountIdentity::PrivatePdaUpdate { (Some(nsk), Some(membership_proof)) => InputAccountIdentity::PrivatePdaUpdate {
ssk: pre.ssk, ssk: pre.ssk,
nsk, nsk,
membership_proof, membership_proof,
identifier: pre.identifier, identifier: pre.identifier,
seed: None,
}, },
_ => InputAccountIdentity::PrivatePdaInit { _ => InputAccountIdentity::PrivatePdaInit {
npk: pre.npk, npk: pre.npk,
ssk: pre.ssk, ssk: pre.ssk,
identifier: pre.identifier, identifier: pre.identifier,
seed: None,
}, },
}, },
State::Private(pre) => match (pre.nsk, pre.proof.clone()) { State::Private(pre) => match (pre.nsk, pre.proof.clone()) {
@ -287,21 +354,66 @@ impl AccountManager {
self.states self.states
.iter() .iter()
.filter_map(|state| match state { .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, State::Private(_) => None,
}) })
.collect() .collect()
} }
pub fn public_account_auth(&self) -> Vec<&PrivateKey> { pub fn public_non_keycard_account_auth(&self) -> Vec<&PrivateKey> {
self.states self.states
.iter() .iter()
.filter_map(|state| match state { .filter_map(|state| match state {
State::Public { sk, .. } => sk.as_ref(), State::Public { sk, .. } => sk.as_ref(),
State::Private(_) => None, State::PublicKeycard { .. } | State::Private(_) => None,
}) })
.collect() .collect()
} }
pub fn sign_message(&self, message_hash: [u8; 32]) -> Result<Vec<(Signature, PublicKey)>> {
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 { struct AccountPreparedData {
@ -410,7 +522,7 @@ mod tests {
#[test] #[test]
fn private_shared_is_private() { fn private_shared_is_private() {
let acc = PrivacyPreservingAccount::PrivateShared { let acc = AccountIdentity::PrivateShared {
nsk: [0; 32], nsk: [0; 32],
npk: NullifierPublicKey([1; 32]), npk: NullifierPublicKey([1; 32]),
vpk: ViewingPublicKey::from_scalar([2; 32]), vpk: ViewingPublicKey::from_scalar([2; 32]),

View File

@ -10,46 +10,39 @@
use std::path::PathBuf; use std::path::PathBuf;
pub use account_manager::AccountIdentity;
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use bip39::Mnemonic; use bip39::Mnemonic;
use common::{HashType, transaction::NSSATransaction}; use common::{HashType, transaction::NSSATransaction};
use config::WalletConfig; use config::WalletConfig;
use key_protocol::key_management::key_tree::chain_index::ChainIndex; use key_protocol::key_management::key_tree::chain_index::ChainIndex;
use keycard_wallet::KeycardWallet;
use log::info; use log::info;
use nssa::{ use nssa::{
Account, AccountId, PrivacyPreservingTransaction, PublicKey, PublicTransaction, Signature, Account, AccountId, PrivacyPreservingTransaction,
privacy_preserving_transaction::{ privacy_preserving_transaction::{
circuit::ProgramWithDependencies, message::EncryptedAccountData, circuit::ProgramWithDependencies, message::EncryptedAccountData,
}, },
program::Program,
public_transaction::WitnessSet as PublicWitnessSet,
}; };
use nssa_core::{ use nssa_core::{
Commitment, MembershipProof, SharedSecretKey, Commitment, MembershipProof, SharedSecretKey, account::Nonce, program::InstructionData,
account::{AccountWithMetadata, Nonce},
program::InstructionData,
}; };
pub use privacy_preserving_tx::PrivacyPreservingAccount;
use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder}; use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder};
use storage::Storage; use storage::Storage;
use tokio::io::AsyncWriteExt as _; use tokio::io::AsyncWriteExt as _;
use crate::{ use crate::{
account::{AccountIdWithPrivacy, Label}, account::{AccountIdWithPrivacy, Label},
cli::CliAccountMention,
config::WalletConfigOverrides, config::WalletConfigOverrides,
poller::TxPoller, poller::TxPoller,
signing::SigningGroup,
storage::key_chain::SharedAccountEntry, storage::key_chain::SharedAccountEntry,
}; };
pub mod account; pub mod account;
mod account_manager;
pub mod cli; pub mod cli;
pub mod config; pub mod config;
pub mod helperfunctions; pub mod helperfunctions;
pub mod poller; pub mod poller;
mod privacy_preserving_tx;
pub mod program_facades; pub mod program_facades;
pub mod signing; pub mod signing;
pub mod storage; pub mod storage;
@ -295,13 +288,10 @@ impl WalletCore {
self.storage.key_chain_mut().set_sealing_secret_key(key); 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. /// Checks the key tree first, then shared private accounts.
#[must_use] #[must_use]
pub fn resolve_private_account( pub fn resolve_private_account(&self, account_id: nssa::AccountId) -> Option<AccountIdentity> {
&self,
account_id: nssa::AccountId,
) -> Option<PrivacyPreservingAccount> {
// Check key tree first // Check key tree first
if self if self
.storage .storage
@ -309,7 +299,7 @@ impl WalletCore {
.private_account(account_id) .private_account(account_id)
.is_some() .is_some()
{ {
return Some(PrivacyPreservingAccount::PrivateOwned(account_id)); return Some(AccountIdentity::PrivateOwned(account_id));
} }
// Check shared private accounts // Check shared private accounts
@ -322,9 +312,9 @@ impl WalletCore {
.key_chain() .key_chain()
.group_key_holder(&entry.group_label)?; .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); let keys = holder.derive_keys_for_pda(&program_id, &pda_seed);
Some(PrivacyPreservingAccount::PrivatePdaShared { Some(AccountIdentity::PrivatePdaShared {
account_id, account_id,
nsk: keys.nullifier_secret_key, nsk: keys.nullifier_secret_key,
npk: keys.generate_nullifier_public_key(), npk: keys.generate_nullifier_public_key(),
@ -341,7 +331,7 @@ impl WalletCore {
result result
}; };
let keys = holder.derive_keys_for_shared_account(&derivation_seed); let keys = holder.derive_keys_for_shared_account(&derivation_seed);
Some(PrivacyPreservingAccount::PrivateShared { Some(AccountIdentity::PrivateShared {
nsk: keys.nullifier_secret_key, nsk: keys.nullifier_secret_key,
npk: keys.generate_nullifier_public_key(), npk: keys.generate_nullifier_public_key(),
vpk: keys.generate_viewing_public_key(), vpk: keys.generate_viewing_public_key(),
@ -365,7 +355,7 @@ impl WalletCore {
group_label: Label, group_label: Label,
identifier: nssa_core::Identifier, identifier: nssa_core::Identifier,
pda_seed: Option<nssa_core::program::PdaSeed>, pda_seed: Option<nssa_core::program::PdaSeed>,
pda_program_id: Option<nssa_core::program::ProgramId>, authority_program_id: Option<nssa_core::program::ProgramId>,
) { ) {
self.storage.key_chain_mut().insert_shared_private_account( self.storage.key_chain_mut().insert_shared_private_account(
account_id, account_id,
@ -373,7 +363,7 @@ impl WalletCore {
group_label, group_label,
identifier, identifier,
pda_seed, pda_seed,
pda_program_id, authority_program_id,
account: Account::default(), account: Account::default(),
}, },
); );
@ -564,145 +554,28 @@ impl WalletCore {
Ok(()) Ok(())
} }
/// Send a public transaction, fetching nonces automatically from
/// [`SigningGroup::signing_ids`].
pub async fn send_public_tx<T: serde::Serialize>(
&self,
program: &Program,
account_ids: Vec<AccountId>,
instruction: T,
groups: SigningGroup,
) -> Result<HashType, ExecutionFailureKind> {
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<T: serde::Serialize>(
&self,
program: &Program,
account_ids: Vec<AccountId>,
nonces: Vec<Nonce>,
instruction: T,
groups: SigningGroup,
) -> Result<HashType, ExecutionFailureKind> {
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( pub async fn send_privacy_preserving_tx(
&self, &self,
accounts: Vec<PrivacyPreservingAccount>, accounts: Vec<AccountIdentity>,
instruction_data: InstructionData, instruction_data: InstructionData,
program: &ProgramWithDependencies, program: &ProgramWithDependencies,
mention: Option<&CliAccountMention>,
) -> Result<(HashType, Vec<SharedSecretKey>), ExecutionFailureKind> { ) -> Result<(HashType, Vec<SharedSecretKey>), ExecutionFailureKind> {
self.send_privacy_preserving_tx_with_pre_check( self.send_privacy_preserving_tx_with_pre_check(accounts, instruction_data, program, |_| {
accounts, Ok(())
instruction_data, })
program,
|_| Ok(()),
mention,
)
.await .await
} }
pub async fn send_privacy_preserving_tx_with_pre_check( pub async fn send_privacy_preserving_tx_with_pre_check(
&self, &self,
accounts: Vec<PrivacyPreservingAccount>, accounts: Vec<AccountIdentity>,
instruction_data: InstructionData, instruction_data: InstructionData,
program: &ProgramWithDependencies, program: &ProgramWithDependencies,
tx_pre_check: impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>, tx_pre_check: impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>,
mention: Option<&CliAccountMention>,
) -> Result<(HashType, Vec<SharedSecretKey>), ExecutionFailureKind> { ) -> Result<(HashType, Vec<SharedSecretKey>), 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 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::<AccountIdWithPrivacy>()
.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<Nonce> = acc_manager.public_account_nonces().into_iter().collect();
let mut account_ids: Vec<AccountId> = 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());
}
}
tx_pre_check( tx_pre_check(
&pre_states &pre_states
@ -717,54 +590,30 @@ impl WalletCore {
instruction_data, instruction_data,
acc_manager.account_identities(), acc_manager.account_identities(),
&program.to_owned(), &program.to_owned(),
) )?;
.unwrap();
let message = let message =
nssa::privacy_preserving_transaction::message::Message::try_from_circuit_output( nssa::privacy_preserving_transaction::message::Message::try_from_circuit_output(
account_ids, acc_manager.public_account_ids(),
nonces, acc_manager.public_account_nonces(),
private_account_keys private_account_keys
.iter() .iter()
.map(|keys| (keys.npk, keys.vpk.clone(), keys.epk.clone())) .map(|keys| (keys.npk, keys.vpk.clone(), keys.epk.clone()))
.collect(), .collect(),
output, 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 = let witness_set =
if let (Some(pin), Some(path)) = (keycard_pin.as_deref(), keycard_path.as_deref()) { nssa::privacy_preserving_transaction::witness_set::WitnessSet::from_raw_parts(
let hash = message.hash(); signatures_public_keys,
let local_auth = acc_manager.public_account_auth(); proof,
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(),
)
};
let tx = PrivacyPreservingTransaction::new(message, witness_set); let tx = PrivacyPreservingTransaction::new(message, witness_set);
let shared_secrets: Vec<_> = private_account_keys let shared_secrets: Vec<_> = private_account_keys
@ -780,6 +629,69 @@ impl WalletCore {
)) ))
} }
pub async fn send_pub_tx(
&self,
accounts: Vec<AccountIdentity>,
instruction_data: InstructionData,
program: &ProgramWithDependencies,
) -> Result<HashType, ExecutionFailureKind> {
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<AccountIdentity>,
instruction_data: InstructionData,
program: &ProgramWithDependencies,
tx_pre_check: impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>,
) -> Result<HashType, ExecutionFailureKind> {
// 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::<Vec<_>>(),
)?;
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<u64> { pub async fn sync_to_latest_block(&mut self) -> Result<u64> {
let latest_block_id = self.sequencer_client.get_last_block_id().await?; let latest_block_id = self.sequencer_client.get_last_block_id().await?;
println!("Latest block is {latest_block_id}"); println!("Latest block is {latest_block_id}");
@ -899,7 +811,7 @@ impl WalletCore {
.key_chain() .key_chain()
.group_key_holder(&entry.group_label)?; .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)) => { (Some(pda_seed), Some(program_id)) => {
holder.derive_keys_for_pda(program_id, pda_seed) holder.derive_keys_for_pda(program_id, pda_seed)
} }

View File

@ -3,7 +3,7 @@ use common::HashType;
use nssa::{AccountId, program::Program}; use nssa::{AccountId, program::Program};
use token_core::TokenHolding; 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); pub struct Amm<'wallet>(pub &'wallet WalletCore);
impl Amm<'_> { impl Amm<'_> {
@ -15,18 +15,36 @@ impl Amm<'_> {
user_holding_lp: AccountId, user_holding_lp: AccountId,
balance_a: u128, balance_a: u128,
balance_b: u128, balance_b: u128,
a_mention: &CliAccountMention, user_holding_a_mention: &CliAccountMention,
b_mention: &CliAccountMention, user_holding_b_mention: &CliAccountMention,
lp_mention: &CliAccountMention, user_holding_lp_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> { ) -> Result<HashType, ExecutionFailureKind> {
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 program = Program::amm();
let amm_program_id = Program::amm().id(); 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 let user_a_acc = self
.0 .0
.get_account_public(user_holding_a) .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_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 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 pool_lp = compute_liquidity_token_pda(amm_program_id, amm_pool);
let instruction = amm_core::Instruction::NewDefinition {
let account_ids = vec![ token_a_amount: balance_a,
amm_pool, token_b_amount: balance_b,
vault_holding_a, amm_program_id,
vault_holding_b, };
pool_lp, let instruction_data =
user_holding_a, Program::serialize_instruction(instruction).expect("Instruction should serialize");
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)?;
self.0 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 .await
} }
@ -81,17 +101,11 @@ impl Amm<'_> {
swap_amount_in: u128, swap_amount_in: u128,
min_amount_out: u128, min_amount_out: u128,
token_definition_id_in: AccountId, token_definition_id_in: AccountId,
a_mention: &CliAccountMention, user_holding_a_mention: &CliAccountMention,
b_mention: &CliAccountMention, user_holding_b_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> { ) -> Result<HashType, ExecutionFailureKind> {
let instruction = amm_core::Instruction::SwapExactInput {
swap_amount_in,
min_amount_out,
token_definition_id_in,
};
let program = Program::amm(); let program = Program::amm();
let amm_program_id = Program::amm().id(); let amm_program_id = Program::amm().id();
let user_a_acc = self let user_a_acc = self
.0 .0
.get_account_public(user_holding_a) .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); 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_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 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![ if (token_definition_id_in != definition_token_a_id)
amm_pool, && (token_definition_id_in != definition_token_b_id)
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 {
return Err(ExecutionFailureKind::AccountDataError( return Err(ExecutionFailureKind::AccountDataError(
token_definition_id_in, 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 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 .await
} }
@ -150,17 +191,11 @@ impl Amm<'_> {
exact_amount_out: u128, exact_amount_out: u128,
max_amount_in: u128, max_amount_in: u128,
token_definition_id_in: AccountId, token_definition_id_in: AccountId,
a_mention: &CliAccountMention, user_holding_a_mention: &CliAccountMention,
b_mention: &CliAccountMention, user_holding_b_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> { ) -> Result<HashType, ExecutionFailureKind> {
let instruction = amm_core::Instruction::SwapExactOutput {
exact_amount_out,
max_amount_in,
token_definition_id_in,
};
let program = Program::amm(); let program = Program::amm();
let amm_program_id = Program::amm().id(); let amm_program_id = Program::amm().id();
let user_a_acc = self let user_a_acc = self
.0 .0
.get_account_public(user_holding_a) .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); 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_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 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![ if (token_definition_id_in != definition_token_a_id)
amm_pool, && (token_definition_id_in != definition_token_b_id)
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 {
return Err(ExecutionFailureKind::AccountDataError( return Err(ExecutionFailureKind::AccountDataError(
token_definition_id_in, 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 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 .await
} }
@ -220,18 +282,36 @@ impl Amm<'_> {
min_amount_liquidity: u128, min_amount_liquidity: u128,
max_amount_to_add_token_a: u128, max_amount_to_add_token_a: u128,
max_amount_to_add_token_b: u128, max_amount_to_add_token_b: u128,
a_mention: &CliAccountMention, user_holding_a_mention: &CliAccountMention,
b_mention: &CliAccountMention, user_holding_b_mention: &CliAccountMention,
lp_mention: &CliAccountMention, user_holding_lp_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> { ) -> Result<HashType, ExecutionFailureKind> {
let instruction = amm_core::Instruction::AddLiquidity { let user_holding_a_identity = user_holding_a_mention.key_path().map_or(
min_amount_liquidity, AccountIdentity::Public(user_holding_a),
max_amount_to_add_token_a, |key_path| AccountIdentity::PublicKeycard {
max_amount_to_add_token_b, 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 program = Program::amm();
let amm_program_id = Program::amm().id(); let amm_program_id = Program::amm().id();
let user_a_acc = self let user_a_acc = self
.0 .0
.get_account_public(user_holding_a) .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_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 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 pool_lp = compute_liquidity_token_pda(amm_program_id, amm_pool);
let instruction = amm_core::Instruction::AddLiquidity {
let account_ids = vec![ min_amount_liquidity,
amm_pool, max_amount_to_add_token_a,
vault_holding_a, max_amount_to_add_token_b,
vault_holding_b, };
pool_lp, let instruction_data =
user_holding_a, Program::serialize_instruction(instruction).expect("Instruction should serialize");
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)?;
self.0 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 .await
} }
@ -287,16 +369,18 @@ impl Amm<'_> {
remove_liquidity_amount: u128, remove_liquidity_amount: u128,
min_amount_to_remove_token_a: u128, min_amount_to_remove_token_a: u128,
min_amount_to_remove_token_b: u128, min_amount_to_remove_token_b: u128,
lp_mention: &CliAccountMention, user_holding_lp_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> { ) -> Result<HashType, ExecutionFailureKind> {
let instruction = amm_core::Instruction::RemoveLiquidity { let user_holding_lp_identity = user_holding_lp_mention.key_path().map_or(
remove_liquidity_amount, AccountIdentity::Public(user_holding_lp),
min_amount_to_remove_token_a, |key_path| AccountIdentity::PublicKeycard {
min_amount_to_remove_token_b, account_id: user_holding_lp,
}; key_path: key_path.to_owned(),
},
);
let program = Program::amm(); let program = Program::amm();
let amm_program_id = Program::amm().id(); let amm_program_id = Program::amm().id();
let user_a_acc = self let user_a_acc = self
.0 .0
.get_account_public(user_holding_a) .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_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 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 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 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 .await
} }
} }

View File

@ -7,10 +7,7 @@ use nssa::{
}; };
use nssa_core::SharedSecretKey; use nssa_core::SharedSecretKey;
use crate::{ use crate::{AccountIdentity, ExecutionFailureKind, WalletCore, cli::CliAccountMention};
ExecutionFailureKind, PrivacyPreservingAccount, WalletCore, cli::CliAccountMention,
signing::SigningGroup,
};
pub struct Ata<'wallet>(pub &'wallet WalletCore); pub struct Ata<'wallet>(pub &'wallet WalletCore);
@ -21,22 +18,36 @@ impl Ata<'_> {
definition_id: AccountId, definition_id: AccountId,
owner_mention: &CliAccountMention, owner_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> { ) -> Result<HashType, ExecutionFailureKind> {
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 program = Program::ata();
let ata_program_id = program.id(); let ata_program_id = program.id();
let ata_id = get_associated_token_account_id( let ata_id = get_associated_token_account_id(
&ata_program_id, &ata_program_id,
&compute_ata_seed(owner_id, definition_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 = 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 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 .await
} }
@ -48,25 +59,39 @@ impl Ata<'_> {
amount: u128, amount: u128,
owner_mention: &CliAccountMention, owner_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> { ) -> Result<HashType, ExecutionFailureKind> {
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 program = Program::ata();
let ata_program_id = program.id(); let ata_program_id = program.id();
let sender_ata_id = get_associated_token_account_id( let sender_ata_id = get_associated_token_account_id(
&ata_program_id, &ata_program_id,
&compute_ata_seed(owner_id, definition_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 { let instruction = ata_core::Instruction::Transfer {
ata_program_id, ata_program_id,
amount, 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 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 .await
} }
@ -77,25 +102,39 @@ impl Ata<'_> {
amount: u128, amount: u128,
owner_mention: &CliAccountMention, owner_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> { ) -> Result<HashType, ExecutionFailureKind> {
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 program = Program::ata();
let ata_program_id = program.id(); let ata_program_id = program.id();
let holder_ata_id = get_associated_token_account_id( let holder_ata_id = get_associated_token_account_id(
&ata_program_id, &ata_program_id,
&compute_ata_seed(owner_id, definition_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 { let instruction = ata_core::Instruction::Burn {
ata_program_id, ata_program_id,
amount, 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 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 .await
} }
@ -118,17 +157,12 @@ impl Ata<'_> {
self.0 self.0
.resolve_private_account(owner_id) .resolve_private_account(owner_id)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?, .ok_or(ExecutionFailureKind::KeyNotFoundError)?,
PrivacyPreservingAccount::Public(definition_id), AccountIdentity::Public(definition_id),
PrivacyPreservingAccount::Public(ata_id), AccountIdentity::Public(ata_id),
]; ];
self.0 self.0
.send_privacy_preserving_tx( .send_privacy_preserving_tx(accounts, instruction_data, &ata_with_token_dependency())
accounts,
instruction_data,
&ata_with_token_dependency(),
None,
)
.await .await
.map(|(hash, mut secrets)| { .map(|(hash, mut secrets)| {
let secret = secrets.pop().expect("expected owner's secret"); let secret = secrets.pop().expect("expected owner's secret");
@ -160,17 +194,12 @@ impl Ata<'_> {
self.0 self.0
.resolve_private_account(owner_id) .resolve_private_account(owner_id)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?, .ok_or(ExecutionFailureKind::KeyNotFoundError)?,
PrivacyPreservingAccount::Public(sender_ata_id), AccountIdentity::Public(sender_ata_id),
PrivacyPreservingAccount::Public(recipient_id), AccountIdentity::Public(recipient_id),
]; ];
self.0 self.0
.send_privacy_preserving_tx( .send_privacy_preserving_tx(accounts, instruction_data, &ata_with_token_dependency())
accounts,
instruction_data,
&ata_with_token_dependency(),
None,
)
.await .await
.map(|(hash, mut secrets)| { .map(|(hash, mut secrets)| {
let secret = secrets.pop().expect("expected owner's secret"); let secret = secrets.pop().expect("expected owner's secret");
@ -201,17 +230,12 @@ impl Ata<'_> {
self.0 self.0
.resolve_private_account(owner_id) .resolve_private_account(owner_id)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?, .ok_or(ExecutionFailureKind::KeyNotFoundError)?,
PrivacyPreservingAccount::Public(holder_ata_id), AccountIdentity::Public(holder_ata_id),
PrivacyPreservingAccount::Public(definition_id), AccountIdentity::Public(definition_id),
]; ];
self.0 self.0
.send_privacy_preserving_tx( .send_privacy_preserving_tx(accounts, instruction_data, &ata_with_token_dependency())
accounts,
instruction_data,
&ata_with_token_dependency(),
None,
)
.await .await
.map(|(hash, mut secrets)| { .map(|(hash, mut secrets)| {
let secret = secrets.pop().expect("expected owner's secret"); let secret = secrets.pop().expect("expected owner's secret");

View File

@ -2,7 +2,7 @@ use common::HashType;
use nssa::AccountId; use nssa::AccountId;
use super::{NativeTokenTransfer, auth_transfer_preparation}; use super::{NativeTokenTransfer, auth_transfer_preparation};
use crate::{ExecutionFailureKind, PrivacyPreservingAccount}; use crate::{AccountIdentity, ExecutionFailureKind};
impl NativeTokenTransfer<'_> { impl NativeTokenTransfer<'_> {
pub async fn send_deshielded_transfer( pub async fn send_deshielded_transfer(
@ -19,12 +19,11 @@ impl NativeTokenTransfer<'_> {
self.0 self.0
.resolve_private_account(from) .resolve_private_account(from)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?, .ok_or(ExecutionFailureKind::KeyNotFoundError)?,
PrivacyPreservingAccount::Public(to), AccountIdentity::Public(to),
], ],
instruction_data, instruction_data,
&program.into(), &program.into(),
tx_pre_check, tx_pre_check,
None,
) )
.await .await
.map(|(resp, secrets)| { .map(|(resp, secrets)| {

View File

@ -5,7 +5,7 @@ use nssa::{AccountId, program::Program};
use nssa_core::{Identifier, NullifierPublicKey, SharedSecretKey, encryption::ViewingPublicKey}; use nssa_core::{Identifier, NullifierPublicKey, SharedSecretKey, encryption::ViewingPublicKey};
use super::{NativeTokenTransfer, auth_transfer_preparation}; use super::{NativeTokenTransfer, auth_transfer_preparation};
use crate::{ExecutionFailureKind, PrivacyPreservingAccount}; use crate::{AccountIdentity, ExecutionFailureKind};
impl NativeTokenTransfer<'_> { impl NativeTokenTransfer<'_> {
pub async fn register_account_private( pub async fn register_account_private(
@ -24,7 +24,6 @@ impl NativeTokenTransfer<'_> {
vec![account], vec![account],
Program::serialize_instruction(instruction).unwrap(), Program::serialize_instruction(instruction).unwrap(),
&Program::authenticated_transfer_program().into(), &Program::authenticated_transfer_program().into(),
None,
) )
.await .await
.map(|(resp, secrets)| { .map(|(resp, secrets)| {
@ -50,7 +49,7 @@ impl NativeTokenTransfer<'_> {
self.0 self.0
.resolve_private_account(from) .resolve_private_account(from)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?, .ok_or(ExecutionFailureKind::KeyNotFoundError)?,
PrivacyPreservingAccount::PrivateForeign { AccountIdentity::PrivateForeign {
npk: to_npk, npk: to_npk,
vpk: to_vpk, vpk: to_vpk,
identifier: to_identifier, identifier: to_identifier,
@ -59,7 +58,6 @@ impl NativeTokenTransfer<'_> {
instruction_data, instruction_data,
&program.into(), &program.into(),
tx_pre_check, tx_pre_check,
None,
) )
.await .await
.map(|(resp, secrets)| { .map(|(resp, secrets)| {
@ -93,7 +91,6 @@ impl NativeTokenTransfer<'_> {
instruction_data, instruction_data,
&program.into(), &program.into(),
tx_pre_check, tx_pre_check,
None,
) )
.await .await
.map(|(resp, secrets)| { .map(|(resp, secrets)| {

View File

@ -3,7 +3,10 @@ use common::HashType;
use nssa::{AccountId, program::Program}; use nssa::{AccountId, program::Program};
use super::NativeTokenTransfer; 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<'_> { impl NativeTokenTransfer<'_> {
pub async fn send_public_transfer( pub async fn send_public_transfer(
@ -14,20 +17,33 @@ impl NativeTokenTransfer<'_> {
from_mention: &CliAccountMention, from_mention: &CliAccountMention,
to_mention: &CliAccountMention, to_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> { ) -> Result<HashType, ExecutionFailureKind> {
let mut groups = SigningGroup::new(); let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move);
groups
.add_required(from_mention, from, self.0) let from_identity =
.and_then(|()| groups.add_optional(to_mention, to, self.0)) from_mention
.map_err(ExecutionFailureKind::from_anyhow)?; .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 self.0
.send_public_tx( .send_pub_tx_with_pre_check(
&Program::authenticated_transfer_program(), vec![from_identity, to_identity],
vec![from, to], instruction_data,
AuthTransferInstruction::Transfer { &program.into(),
amount: balance_to_move, tx_pre_check,
},
groups,
) )
.await .await
} }
@ -37,18 +53,21 @@ impl NativeTokenTransfer<'_> {
from: AccountId, from: AccountId,
account_mention: &CliAccountMention, account_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> { ) -> Result<HashType, ExecutionFailureKind> {
let mut groups = SigningGroup::new(); let from_identity =
groups account_mention
.add_required(account_mention, from, self.0) .key_path()
.map_err(ExecutionFailureKind::from_anyhow)?; .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 self.0
.send_public_tx( .send_pub_tx(vec![from_identity], instruction_data, &program.into())
&Program::authenticated_transfer_program(),
vec![from],
AuthTransferInstruction::Initialize,
groups,
)
.await .await
} }
} }

View File

@ -3,7 +3,7 @@ use nssa::AccountId;
use nssa_core::{Identifier, NullifierPublicKey, SharedSecretKey, encryption::ViewingPublicKey}; use nssa_core::{Identifier, NullifierPublicKey, SharedSecretKey, encryption::ViewingPublicKey};
use super::{NativeTokenTransfer, auth_transfer_preparation}; use super::{NativeTokenTransfer, auth_transfer_preparation};
use crate::{ExecutionFailureKind, PrivacyPreservingAccount, cli::CliAccountMention}; use crate::{AccountIdentity, ExecutionFailureKind, cli::CliAccountMention};
impl NativeTokenTransfer<'_> { impl NativeTokenTransfer<'_> {
pub async fn send_shielded_transfer( pub async fn send_shielded_transfer(
@ -13,11 +13,21 @@ impl NativeTokenTransfer<'_> {
balance_to_move: u128, balance_to_move: u128,
from_mention: &CliAccountMention, from_mention: &CliAccountMention,
) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> { ) -> 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); let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move);
self.0 self.0
.send_privacy_preserving_tx_with_pre_check( .send_privacy_preserving_tx_with_pre_check(
vec![ vec![
PrivacyPreservingAccount::Public(from), from_identity,
self.0 self.0
.resolve_private_account(to) .resolve_private_account(to)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?, .ok_or(ExecutionFailureKind::KeyNotFoundError)?,
@ -25,7 +35,6 @@ impl NativeTokenTransfer<'_> {
instruction_data, instruction_data,
&program.into(), &program.into(),
tx_pre_check, tx_pre_check,
Some(from_mention),
) )
.await .await
.map(|(resp, secrets)| { .map(|(resp, secrets)| {
@ -46,12 +55,22 @@ impl NativeTokenTransfer<'_> {
balance_to_move: u128, balance_to_move: u128,
from_mention: &CliAccountMention, from_mention: &CliAccountMention,
) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> { ) -> 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); let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move);
self.0 self.0
.send_privacy_preserving_tx_with_pre_check( .send_privacy_preserving_tx_with_pre_check(
vec![ vec![
PrivacyPreservingAccount::Public(from), from_identity,
PrivacyPreservingAccount::PrivateForeign { AccountIdentity::PrivateForeign {
npk: to_npk, npk: to_npk,
vpk: to_vpk, vpk: to_vpk,
identifier: to_identifier, identifier: to_identifier,
@ -60,7 +79,6 @@ impl NativeTokenTransfer<'_> {
instruction_data, instruction_data,
&program.into(), &program.into(),
tx_pre_check, tx_pre_check,
Some(from_mention),
) )
.await .await
.map(|(resp, secrets)| { .map(|(resp, secrets)| {

View File

@ -1,9 +1,8 @@
use common::{HashType, transaction::NSSATransaction}; use common::HashType;
use nssa::AccountId; use nssa::{AccountId, program::Program};
use nssa_core::{MembershipProof, SharedSecretKey}; 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); pub struct Pinata<'wallet>(pub &'wallet WalletCore);
@ -14,20 +13,21 @@ impl Pinata<'_> {
winner_account_id: AccountId, winner_account_id: AccountId,
solution: u128, solution: u128,
) -> Result<HashType, ExecutionFailureKind> { ) -> Result<HashType, ExecutionFailureKind> {
let account_ids = vec![pinata_account_id, winner_account_id]; let program = Program::pinata();
let program_id = nssa::program::Program::pinata().id(); let instruction = solution;
let message = let instruction_data =
nssa::public_transaction::Message::try_new(program_id, account_ids, vec![], solution) Program::serialize_instruction(instruction).expect("Instruction should serialize");
.unwrap();
let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]); self.0
let tx = nssa::PublicTransaction::new(message, witness_set); .send_pub_tx(
vec![
Ok(self AccountIdentity::PublicNoSign(pinata_account_id),
.0 AccountIdentity::PublicNoSign(winner_account_id),
.sequencer_client ],
.send_transaction(NSSATransaction::Public(tx)) instruction_data,
.await?) &program.into(),
)
.await
} }
/// Claim a pinata reward using a privacy-preserving transaction for an already-initialized /// Claim a pinata reward using a privacy-preserving transaction for an already-initialized
@ -55,14 +55,13 @@ impl Pinata<'_> {
self.0 self.0
.send_privacy_preserving_tx( .send_privacy_preserving_tx(
vec![ vec![
PrivacyPreservingAccount::Public(pinata_account_id), AccountIdentity::Public(pinata_account_id),
self.0 self.0
.resolve_private_account(winner_account_id) .resolve_private_account(winner_account_id)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?, .ok_or(ExecutionFailureKind::KeyNotFoundError)?,
], ],
nssa::program::Program::serialize_instruction(solution).unwrap(), nssa::program::Program::serialize_instruction(solution).unwrap(),
&nssa::program::Program::pinata().into(), &nssa::program::Program::pinata().into(),
None,
) )
.await .await
.map(|(resp, secrets)| { .map(|(resp, secrets)| {

View File

@ -3,10 +3,7 @@ use nssa::{AccountId, program::Program};
use nssa_core::{Identifier, NullifierPublicKey, SharedSecretKey, encryption::ViewingPublicKey}; use nssa_core::{Identifier, NullifierPublicKey, SharedSecretKey, encryption::ViewingPublicKey};
use token_core::Instruction; use token_core::Instruction;
use crate::{ use crate::{AccountIdentity, ExecutionFailureKind, WalletCore, cli::CliAccountMention};
ExecutionFailureKind, PrivacyPreservingAccount, WalletCore, cli::CliAccountMention,
signing::SigningGroup,
};
pub struct Token<'wallet>(pub &'wallet WalletCore); pub struct Token<'wallet>(pub &'wallet WalletCore);
@ -20,17 +17,33 @@ impl Token<'_> {
definition_mention: &CliAccountMention, definition_mention: &CliAccountMention,
supply_mention: &CliAccountMention, supply_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> { ) -> Result<HashType, ExecutionFailureKind> {
let account_ids = vec![definition_account_id, supply_account_id]; let definition_identity = definition_mention.key_path().map_or(
let instruction = Instruction::NewFungibleDefinition { name, total_supply }; 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(); let supply_identity = supply_mention.key_path().map_or(
groups AccountIdentity::Public(supply_account_id),
.add_required(definition_mention, definition_account_id, self.0) |key_path| AccountIdentity::PublicKeycard {
.and_then(|()| groups.add_required(supply_mention, supply_account_id, self.0)) account_id: supply_account_id,
.map_err(ExecutionFailureKind::from_anyhow)?; 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 self.0
.send_public_tx(&Program::token(), account_ids, instruction, groups) .send_pub_tx(
vec![definition_identity, supply_identity],
instruction_data,
&program.into(),
)
.await .await
} }
@ -48,14 +61,13 @@ impl Token<'_> {
self.0 self.0
.send_privacy_preserving_tx( .send_privacy_preserving_tx(
vec![ vec![
PrivacyPreservingAccount::Public(definition_account_id), AccountIdentity::Public(definition_account_id),
self.0 self.0
.resolve_private_account(supply_account_id) .resolve_private_account(supply_account_id)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?, .ok_or(ExecutionFailureKind::KeyNotFoundError)?,
], ],
instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
None,
) )
.await .await
.map(|(resp, secrets)| { .map(|(resp, secrets)| {
@ -84,11 +96,10 @@ impl Token<'_> {
self.0 self.0
.resolve_private_account(definition_account_id) .resolve_private_account(definition_account_id)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?, .ok_or(ExecutionFailureKind::KeyNotFoundError)?,
PrivacyPreservingAccount::Public(supply_account_id), AccountIdentity::Public(supply_account_id),
], ],
instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
None,
) )
.await .await
.map(|(resp, secrets)| { .map(|(resp, secrets)| {
@ -123,7 +134,6 @@ impl Token<'_> {
], ],
instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
None,
) )
.await .await
.map(|(resp, secrets)| { .map(|(resp, secrets)| {
@ -142,19 +152,35 @@ impl Token<'_> {
sender_mention: &CliAccountMention, sender_mention: &CliAccountMention,
recipient_mention: &CliAccountMention, recipient_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> { ) -> Result<HashType, ExecutionFailureKind> {
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 { let instruction = Instruction::Transfer {
amount_to_transfer: amount, amount_to_transfer: amount,
}; };
let instruction_data =
let mut groups = SigningGroup::new(); Program::serialize_instruction(instruction).expect("Instruction should serialize");
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)?;
self.0 self.0
.send_public_tx(&Program::token(), account_ids, instruction, groups) .send_pub_tx(
vec![sender_identity, recipient_identity],
instruction_data,
&program.into(),
)
.await .await
} }
@ -182,7 +208,6 @@ impl Token<'_> {
], ],
instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
None,
) )
.await .await
.map(|(resp, secrets)| { .map(|(resp, secrets)| {
@ -213,7 +238,7 @@ impl Token<'_> {
self.0 self.0
.resolve_private_account(sender_account_id) .resolve_private_account(sender_account_id)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?, .ok_or(ExecutionFailureKind::KeyNotFoundError)?,
PrivacyPreservingAccount::PrivateForeign { AccountIdentity::PrivateForeign {
npk: recipient_npk, npk: recipient_npk,
vpk: recipient_vpk, vpk: recipient_vpk,
identifier: recipient_identifier, identifier: recipient_identifier,
@ -221,7 +246,6 @@ impl Token<'_> {
], ],
instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
None,
) )
.await .await
.map(|(resp, secrets)| { .map(|(resp, secrets)| {
@ -250,11 +274,10 @@ impl Token<'_> {
self.0 self.0
.resolve_private_account(sender_account_id) .resolve_private_account(sender_account_id)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?, .ok_or(ExecutionFailureKind::KeyNotFoundError)?,
PrivacyPreservingAccount::Public(recipient_account_id), AccountIdentity::Public(recipient_account_id),
], ],
instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
None,
) )
.await .await
.map(|(resp, secrets)| { .map(|(resp, secrets)| {
@ -273,6 +296,14 @@ impl Token<'_> {
amount: u128, amount: u128,
sender_mention: &CliAccountMention, sender_mention: &CliAccountMention,
) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> { ) -> 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 { let instruction = Instruction::Transfer {
amount_to_transfer: amount, amount_to_transfer: amount,
}; };
@ -281,14 +312,13 @@ impl Token<'_> {
self.0 self.0
.send_privacy_preserving_tx( .send_privacy_preserving_tx(
vec![ vec![
PrivacyPreservingAccount::Public(sender_account_id), sender_identity,
self.0 self.0
.resolve_private_account(recipient_account_id) .resolve_private_account(recipient_account_id)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?, .ok_or(ExecutionFailureKind::KeyNotFoundError)?,
], ],
instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
Some(sender_mention),
) )
.await .await
.map(|(resp, secrets)| { .map(|(resp, secrets)| {
@ -309,6 +339,14 @@ impl Token<'_> {
amount: u128, amount: u128,
sender_mention: &CliAccountMention, sender_mention: &CliAccountMention,
) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> { ) -> 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 { let instruction = Instruction::Transfer {
amount_to_transfer: amount, amount_to_transfer: amount,
}; };
@ -317,8 +355,8 @@ impl Token<'_> {
self.0 self.0
.send_privacy_preserving_tx( .send_privacy_preserving_tx(
vec![ vec![
PrivacyPreservingAccount::Public(sender_account_id), sender_identity,
PrivacyPreservingAccount::PrivateForeign { AccountIdentity::PrivateForeign {
npk: recipient_npk, npk: recipient_npk,
vpk: recipient_vpk, vpk: recipient_vpk,
identifier: recipient_identifier, identifier: recipient_identifier,
@ -326,7 +364,6 @@ impl Token<'_> {
], ],
instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
Some(sender_mention),
) )
.await .await
.map(|(resp, secrets)| { .map(|(resp, secrets)| {
@ -345,18 +382,30 @@ impl Token<'_> {
amount: u128, amount: u128,
holder_mention: &CliAccountMention, holder_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> { ) -> Result<HashType, ExecutionFailureKind> {
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 { let instruction = Instruction::Burn {
amount_to_burn: amount, amount_to_burn: amount,
}; };
let instruction_data =
let mut groups = SigningGroup::new(); Program::serialize_instruction(instruction).expect("Instruction should serialize");
groups
.add_required(holder_mention, holder_account_id, self.0)
.map_err(ExecutionFailureKind::from_anyhow)?;
self.0 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 .await
} }
@ -384,7 +433,6 @@ impl Token<'_> {
], ],
instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
None,
) )
.await .await
.map(|(resp, secrets)| { .map(|(resp, secrets)| {
@ -413,11 +461,10 @@ impl Token<'_> {
self.0 self.0
.resolve_private_account(definition_account_id) .resolve_private_account(definition_account_id)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?, .ok_or(ExecutionFailureKind::KeyNotFoundError)?,
PrivacyPreservingAccount::Public(holder_account_id), AccountIdentity::Public(holder_account_id),
], ],
instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
None,
) )
.await .await
.map(|(resp, secrets)| { .map(|(resp, secrets)| {
@ -444,14 +491,13 @@ impl Token<'_> {
self.0 self.0
.send_privacy_preserving_tx( .send_privacy_preserving_tx(
vec![ vec![
PrivacyPreservingAccount::Public(definition_account_id), AccountIdentity::Public(definition_account_id),
self.0 self.0
.resolve_private_account(holder_account_id) .resolve_private_account(holder_account_id)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?, .ok_or(ExecutionFailureKind::KeyNotFoundError)?,
], ],
instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
None,
) )
.await .await
.map(|(resp, secrets)| { .map(|(resp, secrets)| {
@ -471,19 +517,35 @@ impl Token<'_> {
definition_mention: &CliAccountMention, definition_mention: &CliAccountMention,
holder_mention: &CliAccountMention, holder_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> { ) -> Result<HashType, ExecutionFailureKind> {
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 { let instruction = Instruction::Mint {
amount_to_mint: amount, amount_to_mint: amount,
}; };
let instruction_data =
let mut groups = SigningGroup::new(); Program::serialize_instruction(instruction).expect("Instruction should serialize");
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)?;
self.0 self.0
.send_public_tx(&Program::token(), account_ids, instruction, groups) .send_pub_tx(
vec![definition_identity, holder_identity],
instruction_data,
&program.into(),
)
.await .await
} }
@ -511,7 +573,6 @@ impl Token<'_> {
], ],
instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
None,
) )
.await .await
.map(|(resp, secrets)| { .map(|(resp, secrets)| {
@ -542,7 +603,7 @@ impl Token<'_> {
self.0 self.0
.resolve_private_account(definition_account_id) .resolve_private_account(definition_account_id)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?, .ok_or(ExecutionFailureKind::KeyNotFoundError)?,
PrivacyPreservingAccount::PrivateForeign { AccountIdentity::PrivateForeign {
npk: holder_npk, npk: holder_npk,
vpk: holder_vpk, vpk: holder_vpk,
identifier: holder_identifier, identifier: holder_identifier,
@ -550,7 +611,6 @@ impl Token<'_> {
], ],
instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
None,
) )
.await .await
.map(|(resp, secrets)| { .map(|(resp, secrets)| {
@ -579,11 +639,10 @@ impl Token<'_> {
self.0 self.0
.resolve_private_account(definition_account_id) .resolve_private_account(definition_account_id)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?, .ok_or(ExecutionFailureKind::KeyNotFoundError)?,
PrivacyPreservingAccount::Public(holder_account_id), AccountIdentity::Public(holder_account_id),
], ],
instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
None,
) )
.await .await
.map(|(resp, secrets)| { .map(|(resp, secrets)| {
@ -610,14 +669,13 @@ impl Token<'_> {
self.0 self.0
.send_privacy_preserving_tx( .send_privacy_preserving_tx(
vec![ vec![
PrivacyPreservingAccount::Public(definition_account_id), AccountIdentity::Public(definition_account_id),
self.0 self.0
.resolve_private_account(holder_account_id) .resolve_private_account(holder_account_id)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?, .ok_or(ExecutionFailureKind::KeyNotFoundError)?,
], ],
instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
None,
) )
.await .await
.map(|(resp, secrets)| { .map(|(resp, secrets)| {
@ -646,8 +704,8 @@ impl Token<'_> {
self.0 self.0
.send_privacy_preserving_tx( .send_privacy_preserving_tx(
vec![ vec![
PrivacyPreservingAccount::Public(definition_account_id), AccountIdentity::Public(definition_account_id),
PrivacyPreservingAccount::PrivateForeign { AccountIdentity::PrivateForeign {
npk: holder_npk, npk: holder_npk,
vpk: holder_vpk, vpk: holder_vpk,
identifier: holder_identifier, identifier: holder_identifier,
@ -655,7 +713,6 @@ impl Token<'_> {
], ],
instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
None,
) )
.await .await
.map(|(resp, secrets)| { .map(|(resp, secrets)| {

View File

@ -1,119 +1,6 @@
use anyhow::Result;
use keycard_wallet::{KeycardWallet, python_path}; use keycard_wallet::{KeycardWallet, python_path};
use nssa::{AccountId, PrivateKey, PublicKey, Signature};
use pyo3::Python; 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<AccountId> {
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<Vec<(Signature, PublicKey)>> {
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. /// Lazily opens and reuses a single Keycard session for all keycard signers in one transaction.
pub struct KeycardSessionContext { pub struct KeycardSessionContext {
pin: String, pin: String,

View File

@ -55,7 +55,7 @@ pub struct SharedAccountEntry {
/// For PDA accounts, the seed and program ID used to derive keys via `derive_keys_for_pda`. /// 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). /// `None` for regular shared accounts (keys derived from identifier via derivation seed).
pub pda_seed: Option<nssa_core::program::PdaSeed>, pub pda_seed: Option<nssa_core::program::PdaSeed>,
pub pda_program_id: Option<nssa_core::program::ProgramId>, pub authority_program_id: Option<nssa_core::program::ProgramId>,
pub account: Account, pub account: Account,
} }
@ -858,7 +858,7 @@ mod tests {
group_label: Label::new("test-group"), group_label: Label::new("test-group"),
identifier: 42, identifier: 42,
pda_seed: None, pda_seed: None,
pda_program_id: None, authority_program_id: None,
account: nssa_core::account::Account::default(), account: nssa_core::account::Account::default(),
}; };
let encoded = bincode::serialize(&entry).expect("serialize"); let encoded = bincode::serialize(&entry).expect("serialize");
@ -871,7 +871,7 @@ mod tests {
group_label: Label::new("pda-group"), group_label: Label::new("pda-group"),
identifier: u128::MAX, identifier: u128::MAX,
pda_seed: Some(PdaSeed::new([7_u8; 32])), 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(), account: nssa_core::account::Account::default(),
}; };
let pda_encoded = bincode::serialize(&pda_entry).expect("serialize pda"); let pda_encoded = bincode::serialize(&pda_entry).expect("serialize pda");
@ -890,7 +890,7 @@ mod tests {
group_label: Label::new("old"), group_label: Label::new("old"),
identifier: 1, identifier: 1,
pda_seed: None, pda_seed: None,
pda_program_id: None, authority_program_id: None,
account: nssa_core::account::Account::default(), account: nssa_core::account::Account::default(),
}; };
let encoded = bincode::serialize(&entry).expect("serialize"); let encoded = bincode::serialize(&entry).expect("serialize");