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-0141", reason = "`bincode` is unmaintained but continuing to use it." },
{ id = "RUSTSEC-2023-0089", reason = "atomic-polyfill is pulled transitively via risc0-zkvm; waiting on upstream fix (see https://github.com/risc0/risc0/issues/3453)" },
{ id = "RUSTSEC-2026-0097", reason = "`rand` v0.8.5 is present transitively from logos crates, modification may break integration" },
{ id = "RUSTSEC-2026-0118", reason = "`hickory-proto` v0.25.0-alpha.5 is present transitively from logos crates, modification may break integration" },
{ id = "RUSTSEC-2026-0119", reason = "`hickory-proto` v0.25.0-alpha.5 is present transitively from logos crates, modification may break integration" },
{ id = "RUSTSEC-2025-0137", reason = "newest `rint` depends on rustc v1.90.0 and we build artifacts with v1.88.8" },
]
yanked = "deny"
unused-ignored-advisory = "deny"

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
run: rustup install
- name: Restore Rust cache
uses: Swatinem/rust-cache@v2
with:
shared-key: ci-rust-cache
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Lint workspace
env:
RISC0_SKIP_BUILD: "1"
@ -123,6 +129,12 @@ jobs:
- name: Install active toolchain
run: rustup install
- name: Restore Rust cache
uses: Swatinem/rust-cache@v2
with:
shared-key: ci-rust-cache
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install nextest
run: cargo install --locked cargo-nextest
@ -132,9 +144,10 @@ jobs:
RUST_LOG: "info"
run: cargo nextest run --workspace --exclude integration_tests --all-features
integration-tests:
integration-tests-prebuild:
runs-on: ubuntu-latest
timeout-minutes: 90 # TODO: Apply CI cache to speed this up
outputs:
targets: ${{ steps.discover-targets.outputs.targets }}
steps:
- uses: actions/checkout@v5
with:
@ -151,6 +164,75 @@ jobs:
- name: Install active toolchain
run: rustup install
- name: Restore Rust cache
uses: Swatinem/rust-cache@v2
with:
shared-key: ci-rust-cache
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install nextest
run: cargo install --locked cargo-nextest
- name: Build integration test archive
env:
RISC0_DEV_MODE: "1"
run: cargo nextest archive -p integration_tests --archive-file integration-tests.tar.zst --no-pager
- name: Upload integration test archive
uses: actions/upload-artifact@v4
with:
name: integration-tests-archive
path: integration-tests.tar.zst
- name: Discover integration test targets from archive
id: discover-targets
run: |
cargo nextest list \
--archive-file integration-tests.tar.zst \
--list-type binaries-only \
--message-format json \
--no-pager > integration-tests-binaries.json
targets_json="$(jq -c '[."rust-binaries" | to_entries[] | select(.value.kind == "test" and .value."binary-name" != "tps") | .value."binary-name"] | sort | unique' integration-tests-binaries.json)"
if [[ "$targets_json" == "[]" ]]; then
echo "No integration test targets were discovered." >&2
exit 1
fi
echo "targets=$targets_json" >> "$GITHUB_OUTPUT"
echo "Discovered integration targets: $targets_json"
integration-tests:
needs: integration-tests-prebuild
runs-on: ubuntu-latest
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
target: ${{ fromJson(needs.integration-tests-prebuild.outputs.targets) }}
name: integration-tests (${{ matrix.target }})
steps:
- uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- uses: ./.github/actions/install-system-deps
- uses: ./.github/actions/install-risc0
- uses: ./.github/actions/install-logos-blockchain-circuits
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install active toolchain
run: rustup install
- name: Download integration test archive
uses: actions/download-artifact@v4
with:
name: integration-tests-archive
- name: Install nextest
run: cargo install --locked cargo-nextest
@ -158,7 +240,7 @@ jobs:
env:
RISC0_DEV_MODE: "1"
RUST_LOG: "info"
run: cargo nextest run -p integration_tests -- --skip tps_test
run: cargo nextest run --archive-file integration-tests.tar.zst -E "binary(${{ matrix.target }})"
valid-proof-test:
runs-on: ubuntu-latest
@ -179,6 +261,12 @@ jobs:
- name: Install active toolchain
run: rustup install
- name: Restore Rust cache
uses: Swatinem/rust-cache@v2
with:
shared-key: ci-rust-cache
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Test valid proof
env:
RUST_LOG: "info"
@ -196,6 +284,12 @@ jobs:
- uses: ./.github/actions/install-risc0
- name: Restore Rust cache
uses: Swatinem/rust-cache@v2
with:
shared-key: ci-rust-cache
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install just
run: cargo install --locked just

8
Cargo.lock generated
View File

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

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:
```rust
let accounts = vec![PrivacyPreservingAccount::PrivateOwned(account_id)];
let accounts = vec![AccountIdentity::PrivateOwned(account_id)];
// Construct and submit the privacy-preserving transaction
wallet_core

View File

@ -1,5 +1,5 @@
use nssa::{AccountId, program::Program};
use wallet::{PrivacyPreservingAccount, WalletCore};
use wallet::{AccountIdentity, WalletCore};
// Before running this example, compile the `hello_world.rs` guest program with:
//
@ -44,7 +44,7 @@ async fn main() {
// Define the desired greeting in ASCII
let greeting: Vec<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
wallet_core
@ -52,7 +52,6 @@ async fn main() {
accounts,
Program::serialize_instruction(greeting).unwrap(),
&program.into(),
None,
)
.await
.unwrap();

View File

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

View File

@ -2,7 +2,7 @@ use clap::{Parser, Subcommand};
use common::transaction::NSSATransaction;
use nssa::{PublicTransaction, program::Program, public_transaction};
use sequencer_service_rpc::RpcClient as _;
use wallet::{PrivacyPreservingAccount, WalletCore};
use wallet::{AccountIdentity, WalletCore};
// Before running this example, compile the `hello_world_with_move_function.rs` guest program with:
//
@ -99,14 +99,13 @@ async fn main() {
} => {
let instruction: Instruction = (WRITE_FUNCTION_ID, greeting.into_bytes());
let account_id = account_id.parse().unwrap();
let accounts = vec![PrivacyPreservingAccount::PrivateOwned(account_id)];
let accounts = vec![AccountIdentity::PrivateOwned(account_id)];
wallet_core
.send_privacy_preserving_tx(
accounts,
Program::serialize_instruction(instruction).unwrap(),
&program.into(),
None,
)
.await
.unwrap();
@ -139,8 +138,8 @@ async fn main() {
let to = to.parse().unwrap();
let accounts = vec![
PrivacyPreservingAccount::Public(from),
PrivacyPreservingAccount::PrivateOwned(to),
AccountIdentity::Public(from),
AccountIdentity::PrivateOwned(to),
];
wallet_core
@ -148,7 +147,6 @@ async fn main() {
accounts,
Program::serialize_instruction(instruction).unwrap(),
&program.into(),
None,
)
.await
.unwrap();

View File

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

View File

@ -6,27 +6,37 @@
use std::{path::PathBuf, time::Duration};
use anyhow::{Context as _, Result};
use authenticated_transfer_core::Instruction as AuthTransferInstruction;
use common::transaction::NSSATransaction;
use integration_tests::{
NSSA_PROGRAM_FOR_TEST_PDA_FUND_SPEND_PROXY, TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext,
NSSA_PROGRAM_FOR_TEST_PDA_SPEND_PROXY, TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext,
verify_commitment_is_in_state,
};
use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder;
use log::info;
use nssa::{
AccountId, ProgramId, privacy_preserving_transaction::circuit::ProgramWithDependencies,
AccountId, PrivacyPreservingTransaction, ProgramId,
privacy_preserving_transaction::{
circuit::{ProgramWithDependencies, execute_and_prove},
message::Message,
witness_set::WitnessSet,
},
program::Program,
};
use nssa_core::{NullifierPublicKey, encryption::ViewingPublicKey, program::PdaSeed};
use nssa_core::{
InputAccountIdentity, NullifierPublicKey,
account::{Account, AccountWithMetadata},
encryption::ViewingPublicKey,
program::PdaSeed,
};
use sequencer_service_rpc::RpcClient as _;
use tokio::test;
use wallet::{
PrivacyPreservingAccount, WalletCore,
AccountIdentity, WalletCore,
cli::{Command, account::AccountSubcommand},
};
/// Funds a private PDA via the proxy program with a chained call to `auth_transfer`.
///
/// A direct call to `auth_transfer` cannot establish the PDA-to-npk binding because it uses
/// `Claim::Authorized` rather than `Claim::Pda`. Routing through the proxy provides the binding
/// via `pda_seeds` in the chained call to `auth_transfer`.
/// Funds a private PDA by calling `auth_transfer` directly.
#[expect(
clippy::too_many_arguments,
reason = "test helper — grouping args would obscure intent"
@ -34,33 +44,68 @@ use wallet::{
async fn fund_private_pda(
wallet: &WalletCore,
sender: AccountId,
pda_account_id: AccountId,
npk: NullifierPublicKey,
vpk: ViewingPublicKey,
identifier: u128,
seed: PdaSeed,
authority_program_id: ProgramId,
amount: u128,
proxy_program: &ProgramWithDependencies,
auth_transfer_id: ProgramId,
auth_transfer: &ProgramWithDependencies,
) -> Result<()> {
wallet
.send_privacy_preserving_tx(
vec![
PrivacyPreservingAccount::Public(sender),
PrivacyPreservingAccount::PrivatePdaForeign {
account_id: pda_account_id,
npk,
vpk,
identifier,
},
],
Program::serialize_instruction((seed, amount, auth_transfer_id, true))
.context("failed to serialize pda_fund_spend_proxy fund instruction")?,
proxy_program,
None,
)
let pda_account_id = AccountId::for_private_pda(&authority_program_id, &seed, &npk, identifier);
let sender_account = wallet
.get_account_public(sender)
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
.map_err(|e| anyhow::anyhow!("failed to get sender account: {e}"))?;
let sender_sk = wallet
.get_account_public_signing_key(sender)
.context("sender signing key not found")?;
let sender_pre = AccountWithMetadata::new(sender_account.clone(), true, sender);
let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_account_id);
let eph_holder = EphemeralKeyHolder::new(&npk);
let ssk = eph_holder.calculate_shared_secret_sender(&vpk);
let epk = eph_holder.generate_ephemeral_public_key();
let instruction = Program::serialize_instruction(AuthTransferInstruction::Transfer { amount })
.context("failed to serialize auth_transfer instruction")?;
let account_identities = vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivatePdaInit {
npk,
ssk,
identifier,
seed: Some((seed, authority_program_id)),
},
];
let (output, proof) = execute_and_prove(
vec![sender_pre, pda_pre],
instruction,
account_identities,
auth_transfer,
)
.map_err(|e| anyhow::anyhow!("circuit proving failed: {e}"))?;
let message = Message::try_from_circuit_output(
vec![sender],
vec![sender_account.nonce],
vec![(npk, vpk, epk)],
output,
)
.map_err(|e| anyhow::anyhow!("message build failed: {e}"))?;
let witness_set = WitnessSet::for_message(&message, proof, &[sender_sk]);
let tx = PrivacyPreservingTransaction::new(message, witness_set);
wallet
.sequencer_client
.send_transaction(NSSATransaction::PrivacyPreserving(tx))
.await
.map_err(|e| anyhow::anyhow!("send transaction failed: {e}"))?;
Ok(())
}
@ -79,22 +124,21 @@ async fn spend_private_pda(
seed: PdaSeed,
amount: u128,
spend_program: &ProgramWithDependencies,
auth_transfer_id: nssa::ProgramId,
auth_transfer_id: ProgramId,
) -> Result<()> {
wallet
.send_privacy_preserving_tx(
vec![
PrivacyPreservingAccount::PrivatePdaOwned(pda_account_id),
PrivacyPreservingAccount::PrivateForeign {
AccountIdentity::PrivatePdaOwned(pda_account_id),
AccountIdentity::PrivateForeign {
npk: recipient_npk,
vpk: recipient_vpk,
identifier: 0,
},
],
Program::serialize_instruction((seed, amount, auth_transfer_id, false))
.context("failed to serialize pda_fund_spend_proxy instruction")?,
Program::serialize_instruction((seed, amount, auth_transfer_id))
.context("failed to serialize pda_spend_proxy instruction")?,
spend_program,
None,
)
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
@ -126,9 +170,9 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> {
let proxy = {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../artifacts/test_program_methods")
.join(NSSA_PROGRAM_FOR_TEST_PDA_FUND_SPEND_PROXY);
.join(NSSA_PROGRAM_FOR_TEST_PDA_SPEND_PROXY);
Program::new(std::fs::read(&path).with_context(|| format!("reading {path:?}"))?)
.context("invalid pda_fund_spend_proxy binary")?
.context("invalid pda_spend_proxy binary")?
};
let auth_transfer = Program::authenticated_transfer_program();
let proxy_id = proxy.id();
@ -136,6 +180,7 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> {
let seed = PdaSeed::new([42; 32]);
let amount: u128 = 100;
let auth_transfer_program = ProgramWithDependencies::new(auth_transfer.clone(), [].into());
let spend_program =
ProgramWithDependencies::new(proxy, [(auth_transfer_id, auth_transfer)].into());
@ -153,14 +198,13 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> {
fund_private_pda(
ctx.wallet(),
sender_0,
alice_pda_0_id,
alice_npk,
alice_vpk.clone(),
0,
seed,
proxy_id,
amount,
&spend_program,
auth_transfer_id,
&auth_transfer_program,
)
.await?;
@ -168,14 +212,13 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> {
fund_private_pda(
ctx.wallet(),
sender_1,
alice_pda_1_id,
alice_npk,
alice_vpk.clone(),
1,
seed,
proxy_id,
amount,
&spend_program,
auth_transfer_id,
&auth_transfer_program,
)
.await?;

View File

@ -5,7 +5,7 @@ use crate::{
NullifierSecretKey, SharedSecretKey,
account::{Account, AccountWithMetadata},
encryption::Ciphertext,
program::{BlockValidityWindow, ProgramId, ProgramOutput, TimestampValidityWindow},
program::{BlockValidityWindow, PdaSeed, ProgramId, ProgramOutput, TimestampValidityWindow},
};
#[derive(Serialize, Deserialize)]
@ -60,15 +60,28 @@ pub enum InputAccountIdentity {
npk: NullifierPublicKey,
ssk: SharedSecretKey,
identifier: Identifier,
/// When `Some((seed, authority_program_id))`, the circuit binds this position via the
/// external derivation check
/// `AccountId::for_private_pda(authority_program_id, seed, npk, identifier) ==
/// pre_state.account_id` rather than requiring a `Claim::Pda` or caller
/// `pda_seeds` to establish the binding. The `pre_state` must have `is_authorized
/// == false`.
seed: Option<(PdaSeed, ProgramId)>,
},
/// Update of an existing private PDA, authorized, with membership proof. `npk` is derived
/// from `nsk`. Authorization is established upstream by a caller `pda_seeds` match or a
/// Update of an existing private PDA, with membership proof. `npk` is derived
/// from `nsk`. Authorization may be established upstream by a caller `pda_seeds` match or a
/// previously-seen authorization in a chained call.
PrivatePdaUpdate {
ssk: SharedSecretKey,
nsk: NullifierSecretKey,
membership_proof: MembershipProof,
identifier: Identifier,
/// When `Some((seed, authority_program_id))`, the circuit binds this position via the
/// external derivation check
/// `AccountId::for_private_pda(authority_program_id, seed, npk, identifier) ==
/// pre_state.account_id` rather than requiring a caller `pda_seeds` to establish
/// the binding. The `pre_state` must have `is_authorized == false`.
seed: Option<(PdaSeed, ProgramId)>,
},
}

View File

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

View File

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

View File

@ -2218,7 +2218,7 @@ pub mod tests {
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
}
/// A mask-3 account that no program claims via `Claim::Pda` and no caller authorizes via
/// A private PDA account that no program claims via `Claim::Pda` and no caller authorizes via
/// `ChainedCall.pda_seeds` has no binding between its supplied npk and its `account_id`,
/// so the circuit must reject. Here `simple_balance_transfer` emits no claim for the
/// second account, leaving position 1 unbound.
@ -2249,6 +2249,7 @@ pub mod tests {
npk,
ssk: shared_secret,
identifier: u128::MAX,
seed: None,
},
],
&program.into(),
@ -2257,7 +2258,7 @@ pub mod tests {
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
}
/// Happy path: a program claims a new mask-3 account via `Claim::Pda(seed)`. The circuit
/// Happy path: a program claims a new private PDA via `Claim::Pda(seed)`. The circuit
/// reads the npk for that `pre_state` from `private_account_keys` at the `pre_state`'s
/// position, derives `AccountId` via `AccountId::for_private_pda(program_id, seed, npk)`, and
/// asserts it equals the `pre_state`'s `account_id`. The equality both validates the claim
@ -2280,11 +2281,12 @@ pub mod tests {
npk,
ssk: shared_secret,
identifier: u128::MAX,
seed: None,
}],
&program.into(),
);
let (output, _proof) = result.expect("mask-3 private PDA claim should succeed");
let (output, _proof) = result.expect("private PDA claim should succeed");
assert_eq!(output.new_nullifiers.len(), 1);
assert_eq!(output.new_commitments.len(), 1);
assert_eq!(output.ciphertexts.len(), 1);
@ -2319,6 +2321,7 @@ pub mod tests {
npk: npk_b,
ssk: shared_secret,
identifier: u128::MAX,
seed: None,
}],
&program.into(),
);
@ -2326,7 +2329,7 @@ pub mod tests {
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
}
/// Happy path for the caller-seeds authorization of a mask-3 PDA. The delegator claims a
/// Happy path for the caller-seeds authorization of a private PDA. The delegator claims a
/// private PDA via `Claim::Pda(seed)`, then chains to a callee (`noop`) delegating the same
/// seed via `ChainedCall.pda_seeds`. In the callee's step, the `pre_state`'s authorization
/// is established via the private derivation
@ -2354,12 +2357,13 @@ pub mod tests {
npk,
ssk: shared_secret,
identifier: u128::MAX,
seed: None,
}],
&program_with_deps,
);
let (output, _proof) =
result.expect("caller-seeds authorization of mask-3 private PDA should succeed");
result.expect("caller-seeds authorization of private PDA should succeed");
assert_eq!(output.new_commitments.len(), 1);
assert_eq!(output.new_nullifiers.len(), 1);
}
@ -2392,6 +2396,7 @@ pub mod tests {
npk,
ssk: shared_secret,
identifier: u128::MAX,
seed: None,
}],
&program_with_deps,
);
@ -2401,8 +2406,8 @@ pub mod tests {
/// Exploit-scenario pin. A single `(program_id, seed)` pair can derive a family of
/// `AccountId`s, one public PDA and one private PDA per distinct npk. Without the tx-wide
/// family-binding check, a program could claim `PDA_alice` (mask-3, `alice_npk`) and
/// `PDA_bob` (mask-3, `bob_npk`) under the same seed in one transaction, and once reuse
/// family-binding check, a program could claim `PDA_alice` (`alice_npk`) and
/// `PDA_bob` (`bob_npk`) under the same seed in one transaction, and once reuse
/// is supported a later chained call could delegate both to a callee via
/// `pda_seeds: [S]` and mix balances across them. The binding check rejects the setup
/// here: after the first claim records `(program, seed) → PDA_alice`, the second claim
@ -2430,11 +2435,13 @@ pub mod tests {
npk: keys_a.npk(),
ssk: shared_a,
identifier: u128::MAX,
seed: None,
},
InputAccountIdentity::PrivatePdaInit {
npk: keys_b.npk(),
ssk: shared_b,
identifier: u128::MAX,
seed: None,
},
],
&program.into(),
@ -2443,17 +2450,11 @@ pub mod tests {
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
}
/// Pins the current limitation: a mask-3 PDA that was claimed in a previous transaction
/// cannot be re-used in a new transaction as-is. This PR only binds supplied npks via a
/// fresh `Claim::Pda` or a caller's `ChainedCall.pda_seeds`, neither is present when a
/// program operates on an already-owned private PDA at top level. The reject site is the
/// post-loop `private_pda_bound_positions` assertion in
/// `privacy_preserving_circuit.rs`: `noop` emits no `Claim::Pda` and there is no caller
/// A private PDA that is reused at top level without an external seed in the identity still
/// fails binding. The noop program emits no `Claim::Pda` and there is no caller
/// `ChainedCall.pda_seeds`, so position 0 is never bound and the assertion fires.
// TODO: a follow-up PR in the Private PDAs series needs to let the wallet supply a
// `(seed, original_owner_program_id)` side input per mask-3 `pre_state` so the circuit
// can re-verify `AccountId::for_private_pda(owner, seed, npk) == pre.account_id` without a
// claim.
/// Supplying `seed: Some((seed, owner_program_id))` in the `PrivatePdaUpdate` identity is
/// the correct path for top-level reuse; this test pins the failure when no seed is provided.
#[test]
fn private_pda_top_level_reuse_rejected_by_binding_check() {
let program = Program::noop();
@ -2481,6 +2482,7 @@ pub mod tests {
npk,
ssk: shared_secret,
identifier: u128::MAX,
seed: None,
}],
&program.into(),
);
@ -4372,15 +4374,15 @@ pub mod tests {
let alice_keys = test_private_account_keys_1();
let alice_npk = alice_keys.npk();
let proxy = Program::pda_fund_spend_proxy();
let proxy = Program::pda_spend_proxy();
let auth_transfer = Program::authenticated_transfer_program();
let proxy_id = proxy.id();
let auth_transfer_id = auth_transfer.id();
let seed = PdaSeed::new([42; 32]);
let amount: u128 = 100;
let program_with_deps =
ProgramWithDependencies::new(proxy, [(auth_transfer_id, auth_transfer)].into());
let spend_with_deps =
ProgramWithDependencies::new(proxy, [(auth_transfer_id, auth_transfer.clone())].into());
let funder_id = funder_keys.account_id();
let alice_pda_0_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, 0);
@ -4406,7 +4408,7 @@ pub mod tests {
let alice_shared_0 = SharedSecretKey::new([10; 32], &alice_keys.vpk());
let alice_shared_1 = SharedSecretKey::new([11; 32], &alice_keys.vpk());
// Fund alice_pda_0
// Fund alice_pda_0 via authenticated_transfer directly.
{
let funder_account = state.get_account_by_id(funder_id);
let funder_nonce = funder_account.nonce;
@ -4415,16 +4417,18 @@ pub mod tests {
AccountWithMetadata::new(funder_account, true, funder_id),
AccountWithMetadata::new(Account::default(), false, alice_pda_0_id),
],
Program::serialize_instruction((seed, amount, auth_transfer_id, true)).unwrap(),
Program::serialize_instruction(AuthTransferInstruction::Transfer { amount })
.unwrap(),
vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivatePdaInit {
npk: alice_npk,
ssk: alice_shared_0,
identifier: 0,
seed: Some((seed, proxy_id)),
},
],
&program_with_deps,
&auth_transfer.clone().into(),
)
.unwrap();
let message = Message::try_from_circuit_output(
@ -4448,7 +4452,7 @@ pub mod tests {
.unwrap();
}
// Fund alice_pda_1
// Fund alice_pda_1 the same way with identifier 1.
{
let funder_account = state.get_account_by_id(funder_id);
let funder_nonce = funder_account.nonce;
@ -4457,16 +4461,18 @@ pub mod tests {
AccountWithMetadata::new(funder_account, true, funder_id),
AccountWithMetadata::new(Account::default(), false, alice_pda_1_id),
],
Program::serialize_instruction((seed, amount, auth_transfer_id, true)).unwrap(),
Program::serialize_instruction(AuthTransferInstruction::Transfer { amount })
.unwrap(),
vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivatePdaInit {
npk: alice_npk,
ssk: alice_shared_1,
identifier: 1,
seed: Some((seed, proxy_id)),
},
],
&program_with_deps,
&auth_transfer.into(),
)
.unwrap();
let message = Message::try_from_circuit_output(
@ -4504,7 +4510,7 @@ pub mod tests {
AccountWithMetadata::new(alice_pda_0_account, true, alice_pda_0_id),
AccountWithMetadata::new(recipient_account, true, recipient_id),
],
Program::serialize_instruction((seed, amount, auth_transfer_id, false)).unwrap(),
Program::serialize_instruction((seed, amount, auth_transfer_id)).unwrap(),
vec![
InputAccountIdentity::PrivatePdaUpdate {
ssk: alice_shared_0,
@ -4513,10 +4519,11 @@ pub mod tests {
.get_proof_for_commitment(&commitment_pda_0)
.expect("pda_0 must be in state"),
identifier: 0,
seed: None,
},
InputAccountIdentity::Public,
],
&program_with_deps,
&spend_with_deps,
)
.unwrap();
let message = Message::try_from_circuit_output(
@ -4545,10 +4552,10 @@ pub mod tests {
let recipient_account = state.get_account_by_id(recipient_id);
let (output, proof) = execute_and_prove(
vec![
AccountWithMetadata::new(alice_pda_1_account, true, alice_pda_1_id),
AccountWithMetadata::new(alice_pda_1_account.clone(), true, alice_pda_1_id),
AccountWithMetadata::new(recipient_account, false, recipient_id),
],
Program::serialize_instruction((seed, amount, auth_transfer_id, false)).unwrap(),
Program::serialize_instruction((seed, amount, auth_transfer_id)).unwrap(),
vec![
InputAccountIdentity::PrivatePdaUpdate {
ssk: alice_shared_1,
@ -4557,10 +4564,11 @@ pub mod tests {
.get_proof_for_commitment(&commitment_pda_1)
.expect("pda_1 must be in state"),
identifier: 1,
seed: None,
},
InputAccountIdentity::Public,
],
&program_with_deps,
&spend_with_deps,
)
.unwrap();
let message = Message::try_from_circuit_output(
@ -4585,5 +4593,70 @@ pub mod tests {
}
assert_eq!(state.get_account_by_id(recipient_id).balance, 2 * amount);
// Re-fund alice_pda_1 top-level via auth_transfer using PrivatePdaUpdate with an
// external seed.
let alice_pda_1_account_after_spend = Account {
program_owner: auth_transfer_id,
balance: 0,
nonce: alice_pda_1_account
.nonce
.private_account_nonce_increment(&alice_keys.nsk),
..Account::default()
};
let commitment_pda_1_after_spend =
Commitment::new(&alice_pda_1_id, &alice_pda_1_account_after_spend);
let alice_shared_1_refund = SharedSecretKey::new([12; 32], &alice_keys.vpk());
{
let recipient_account = state.get_account_by_id(recipient_id);
let recipient_nonce = recipient_account.nonce;
let (output, proof) = execute_and_prove(
vec![
AccountWithMetadata::new(recipient_account, true, recipient_id),
AccountWithMetadata::new(
alice_pda_1_account_after_spend,
false,
alice_pda_1_id,
),
],
Program::serialize_instruction(AuthTransferInstruction::Transfer { amount })
.unwrap(),
vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivatePdaUpdate {
nsk: alice_keys.nsk,
ssk: alice_shared_1_refund,
membership_proof: state
.get_proof_for_commitment(&commitment_pda_1_after_spend)
.expect("pda_1 after spend must be in state"),
identifier: 1,
seed: Some((seed, proxy_id)),
},
],
&Program::authenticated_transfer_program().into(),
)
.unwrap();
let message = Message::try_from_circuit_output(
vec![recipient_id],
vec![recipient_nonce],
vec![(
alice_npk,
alice_keys.vpk(),
EphemeralPublicKey::from_scalar([12; 32]),
)],
output,
)
.unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_signing_key]);
state
.transition_from_privacy_preserving_transaction(
&PrivacyPreservingTransaction::new(message, witness_set),
5,
0,
)
.unwrap();
}
assert_eq!(state.get_account_by_id(recipient_id).balance, amount);
}
}

View File

@ -305,6 +305,68 @@ impl ExecutionState {
}
Entry::Vacant(_) => {
// Pre state for the initial call
let pre_state_position = self.pre_states.len();
let external_seed = match account_identities.get(pre_state_position) {
Some(InputAccountIdentity::PrivatePdaInit {
npk,
identifier,
seed: Some((seed, authority_program_id)),
..
}) => {
let expected = AccountId::for_private_pda(
authority_program_id,
seed,
npk,
*identifier,
);
assert_eq!(
pre_account_id, expected,
"External seed mismatch for PrivatePdaInit at position {pre_state_position}"
);
Some((*seed, *authority_program_id))
}
Some(InputAccountIdentity::PrivatePdaUpdate {
nsk,
identifier,
seed: Some((seed, authority_program_id)),
..
}) => {
let npk = NullifierPublicKey::from(nsk);
let expected = AccountId::for_private_pda(
authority_program_id,
seed,
&npk,
*identifier,
);
assert_eq!(
pre_account_id, expected,
"External seed mismatch for PrivatePdaUpdate at position {pre_state_position}"
);
Some((*seed, *authority_program_id))
}
_ => None,
};
// External seed is only consulted the first time the account is seen.
// Subsequent calls need no re-check because the entry is already recorded on
// private_pda_bound_positions.
if let Some((seed, authority_program_id)) = external_seed {
assert!(
!pre.is_authorized,
"Private PDA with externally-provided seed must not be authorized at position {pre_state_position}"
);
bind_private_pda_position(
&mut self.private_pda_bound_positions,
pre_state_position,
authority_program_id,
seed,
);
assert_family_binding(
&mut self.pda_family_binding,
authority_program_id,
seed,
pre_account_id,
);
}
self.pre_states.push(pre);
}
}
@ -348,14 +410,11 @@ impl ExecutionState {
);
}
}
} else if account_identity.is_private_pda() {
} else {
// Private accounts: don't enforce the claim semantics. Unauthorized private
// claiming is intentionally allowed
match claim {
Claim::Authorized => {
assert!(
pre_is_authorized,
"Cannot claim unauthorized private PDA {pre_account_id}"
);
}
Claim::Authorized => {}
Claim::Pda(seed) => {
let (npk, identifier) = self
.private_pda_npk_by_position
@ -383,10 +442,6 @@ impl ExecutionState {
);
}
}
} else {
// Standalone private accounts: don't enforce the claim semantics.
// Unauthorized private claiming is intentionally allowed since operating
// these accounts requires the npk/nsk keypair anyway.
}
post.account_mut().program_owner = program_id;

View File

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

View File

@ -34,7 +34,7 @@ pub mod setup;
pub const TIME_TO_WAIT_FOR_BLOCK_SECONDS: u64 = 12;
pub const NSSA_PROGRAM_FOR_TEST_DATA_CHANGER: &str = "data_changer.bin";
pub const NSSA_PROGRAM_FOR_TEST_NOOP: &str = "noop.bin";
pub const NSSA_PROGRAM_FOR_TEST_PDA_FUND_SPEND_PROXY: &str = "pda_fund_spend_proxy.bin";
pub const NSSA_PROGRAM_FOR_TEST_PDA_SPEND_PROXY: &str = "pda_spend_proxy.bin";
pub(crate) const BEDROCK_SERVICE_WITH_OPEN_PORT: &str = "logos-blockchain-node-0";
pub(crate) const BEDROCK_SERVICE_PORT: u16 = 18080;

View File

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

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;
const TX_INCLUSION_POLL_INTERVAL: Duration = Duration::from_millis(250);
const TX_INCLUSION_TIMEOUT: Duration = Duration::from_secs(120);
const TX_INCLUSION_TIMEOUT: Duration = Duration::from_mins(2);
/// Borsh-serialized sizes for one zone block fetched after a step. `block_bytes`
/// is the full Block (header + body + bedrock metadata) and is the closest

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ use common::HashType;
use nssa::{AccountId, program::Program};
use token_core::TokenHolding;
use crate::{ExecutionFailureKind, WalletCore, cli::CliAccountMention, signing::SigningGroup};
use crate::{AccountIdentity, ExecutionFailureKind, WalletCore, cli::CliAccountMention};
pub struct Amm<'wallet>(pub &'wallet WalletCore);
impl Amm<'_> {
@ -15,18 +15,36 @@ impl Amm<'_> {
user_holding_lp: AccountId,
balance_a: u128,
balance_b: u128,
a_mention: &CliAccountMention,
b_mention: &CliAccountMention,
lp_mention: &CliAccountMention,
user_holding_a_mention: &CliAccountMention,
user_holding_b_mention: &CliAccountMention,
user_holding_lp_mention: &CliAccountMention,
) -> Result<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 amm_program_id = Program::amm().id();
let instruction = amm_core::Instruction::NewDefinition {
token_a_amount: balance_a,
token_b_amount: balance_b,
amm_program_id,
};
let user_a_acc = self
.0
.get_account_public(user_holding_a)
@ -50,26 +68,28 @@ impl Amm<'_> {
let vault_holding_a = compute_vault_pda(amm_program_id, amm_pool, definition_token_a_id);
let vault_holding_b = compute_vault_pda(amm_program_id, amm_pool, definition_token_b_id);
let pool_lp = compute_liquidity_token_pda(amm_program_id, amm_pool);
let account_ids = vec![
amm_pool,
vault_holding_a,
vault_holding_b,
pool_lp,
user_holding_a,
user_holding_b,
user_holding_lp,
];
let mut groups = SigningGroup::new();
groups
.add_required(a_mention, user_holding_a, self.0)
.and_then(|()| groups.add_required(b_mention, user_holding_b, self.0))
.and_then(|()| groups.add_optional(lp_mention, user_holding_lp, self.0))
.map_err(ExecutionFailureKind::from_anyhow)?;
let instruction = amm_core::Instruction::NewDefinition {
token_a_amount: balance_a,
token_b_amount: balance_b,
amm_program_id,
};
let instruction_data =
Program::serialize_instruction(instruction).expect("Instruction should serialize");
self.0
.send_public_tx(&program, account_ids, instruction, groups)
.send_pub_tx(
vec![
AccountIdentity::PublicNoSign(amm_pool),
AccountIdentity::PublicNoSign(vault_holding_a),
AccountIdentity::PublicNoSign(vault_holding_b),
AccountIdentity::PublicNoSign(pool_lp),
user_holding_a_identity,
user_holding_b_identity,
user_holding_lp_identity,
],
instruction_data,
&program.into(),
)
.await
}
@ -81,17 +101,11 @@ impl Amm<'_> {
swap_amount_in: u128,
min_amount_out: u128,
token_definition_id_in: AccountId,
a_mention: &CliAccountMention,
b_mention: &CliAccountMention,
user_holding_a_mention: &CliAccountMention,
user_holding_b_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> {
let instruction = amm_core::Instruction::SwapExactInput {
swap_amount_in,
min_amount_out,
token_definition_id_in,
};
let program = Program::amm();
let amm_program_id = Program::amm().id();
let user_a_acc = self
.0
.get_account_public(user_holding_a)
@ -114,31 +128,58 @@ impl Amm<'_> {
compute_pool_pda(amm_program_id, definition_token_a_id, definition_token_b_id);
let vault_holding_a = compute_vault_pda(amm_program_id, amm_pool, definition_token_a_id);
let vault_holding_b = compute_vault_pda(amm_program_id, amm_pool, definition_token_b_id);
let instruction = amm_core::Instruction::SwapExactInput {
swap_amount_in,
min_amount_out,
token_definition_id_in,
};
let instruction_data =
Program::serialize_instruction(instruction).expect("Instruction should serialize");
let account_ids = vec![
amm_pool,
vault_holding_a,
vault_holding_b,
user_holding_a,
user_holding_b,
];
let (account_id_auth, seller_mention) = if definition_token_a_id == token_definition_id_in {
(user_holding_a, a_mention)
} else if definition_token_b_id == token_definition_id_in {
(user_holding_b, b_mention)
} else {
if (token_definition_id_in != definition_token_a_id)
&& (token_definition_id_in != definition_token_b_id)
{
return Err(ExecutionFailureKind::AccountDataError(
token_definition_id_in,
));
}
let user_a_signing_identity = if token_definition_id_in == definition_token_a_id {
user_holding_a_mention.key_path().map_or(
AccountIdentity::Public(user_holding_a),
|key_path| AccountIdentity::PublicKeycard {
account_id: user_holding_a,
key_path: key_path.to_owned(),
},
)
} else {
AccountIdentity::PublicNoSign(user_holding_a)
};
let user_b_signing_identity = if token_definition_id_in == definition_token_b_id {
user_holding_b_mention.key_path().map_or(
AccountIdentity::Public(user_holding_b),
|key_path| AccountIdentity::PublicKeycard {
account_id: user_holding_b,
key_path: key_path.to_owned(),
},
)
} else {
AccountIdentity::PublicNoSign(user_holding_b)
};
let mut groups = SigningGroup::new();
groups
.add_required(seller_mention, account_id_auth, self.0)
.map_err(ExecutionFailureKind::from_anyhow)?;
self.0
.send_public_tx(&program, account_ids, instruction, groups)
.send_pub_tx(
vec![
AccountIdentity::PublicNoSign(amm_pool),
AccountIdentity::PublicNoSign(vault_holding_a),
AccountIdentity::PublicNoSign(vault_holding_b),
user_a_signing_identity,
user_b_signing_identity,
],
instruction_data,
&program.into(),
)
.await
}
@ -150,17 +191,11 @@ impl Amm<'_> {
exact_amount_out: u128,
max_amount_in: u128,
token_definition_id_in: AccountId,
a_mention: &CliAccountMention,
b_mention: &CliAccountMention,
user_holding_a_mention: &CliAccountMention,
user_holding_b_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> {
let instruction = amm_core::Instruction::SwapExactOutput {
exact_amount_out,
max_amount_in,
token_definition_id_in,
};
let program = Program::amm();
let amm_program_id = Program::amm().id();
let user_a_acc = self
.0
.get_account_public(user_holding_a)
@ -183,31 +218,58 @@ impl Amm<'_> {
compute_pool_pda(amm_program_id, definition_token_a_id, definition_token_b_id);
let vault_holding_a = compute_vault_pda(amm_program_id, amm_pool, definition_token_a_id);
let vault_holding_b = compute_vault_pda(amm_program_id, amm_pool, definition_token_b_id);
let instruction = amm_core::Instruction::SwapExactOutput {
exact_amount_out,
max_amount_in,
token_definition_id_in,
};
let instruction_data =
Program::serialize_instruction(instruction).expect("Instruction should serialize");
let account_ids = vec![
amm_pool,
vault_holding_a,
vault_holding_b,
user_holding_a,
user_holding_b,
];
let (account_id_auth, seller_mention) = if definition_token_a_id == token_definition_id_in {
(user_holding_a, a_mention)
} else if definition_token_b_id == token_definition_id_in {
(user_holding_b, b_mention)
} else {
if (token_definition_id_in != definition_token_a_id)
&& (token_definition_id_in != definition_token_b_id)
{
return Err(ExecutionFailureKind::AccountDataError(
token_definition_id_in,
));
}
let user_a_signing_identity = if token_definition_id_in == definition_token_a_id {
user_holding_a_mention.key_path().map_or(
AccountIdentity::Public(user_holding_a),
|key_path| AccountIdentity::PublicKeycard {
account_id: user_holding_a,
key_path: key_path.to_owned(),
},
)
} else {
AccountIdentity::PublicNoSign(user_holding_a)
};
let user_b_signing_identity = if token_definition_id_in == definition_token_b_id {
user_holding_b_mention.key_path().map_or(
AccountIdentity::Public(user_holding_b),
|key_path| AccountIdentity::PublicKeycard {
account_id: user_holding_b,
key_path: key_path.to_owned(),
},
)
} else {
AccountIdentity::PublicNoSign(user_holding_b)
};
let mut groups = SigningGroup::new();
groups
.add_required(seller_mention, account_id_auth, self.0)
.map_err(ExecutionFailureKind::from_anyhow)?;
self.0
.send_public_tx(&program, account_ids, instruction, groups)
.send_pub_tx(
vec![
AccountIdentity::PublicNoSign(amm_pool),
AccountIdentity::PublicNoSign(vault_holding_a),
AccountIdentity::PublicNoSign(vault_holding_b),
user_a_signing_identity,
user_b_signing_identity,
],
instruction_data,
&program.into(),
)
.await
}
@ -220,18 +282,36 @@ impl Amm<'_> {
min_amount_liquidity: u128,
max_amount_to_add_token_a: u128,
max_amount_to_add_token_b: u128,
a_mention: &CliAccountMention,
b_mention: &CliAccountMention,
lp_mention: &CliAccountMention,
user_holding_a_mention: &CliAccountMention,
user_holding_b_mention: &CliAccountMention,
user_holding_lp_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> {
let instruction = amm_core::Instruction::AddLiquidity {
min_amount_liquidity,
max_amount_to_add_token_a,
max_amount_to_add_token_b,
};
let user_holding_a_identity = user_holding_a_mention.key_path().map_or(
AccountIdentity::Public(user_holding_a),
|key_path| AccountIdentity::PublicKeycard {
account_id: user_holding_a,
key_path: key_path.to_owned(),
},
);
let user_holding_b_identity = user_holding_b_mention.key_path().map_or(
AccountIdentity::Public(user_holding_b),
|key_path| AccountIdentity::PublicKeycard {
account_id: user_holding_b,
key_path: key_path.to_owned(),
},
);
let user_holding_lp_identity = user_holding_lp_mention.key_path().map_or(
AccountIdentity::Public(user_holding_lp),
|key_path| AccountIdentity::PublicKeycard {
account_id: user_holding_lp,
key_path: key_path.to_owned(),
},
);
let program = Program::amm();
let amm_program_id = Program::amm().id();
let user_a_acc = self
.0
.get_account_public(user_holding_a)
@ -255,26 +335,28 @@ impl Amm<'_> {
let vault_holding_a = compute_vault_pda(amm_program_id, amm_pool, definition_token_a_id);
let vault_holding_b = compute_vault_pda(amm_program_id, amm_pool, definition_token_b_id);
let pool_lp = compute_liquidity_token_pda(amm_program_id, amm_pool);
let account_ids = vec![
amm_pool,
vault_holding_a,
vault_holding_b,
pool_lp,
user_holding_a,
user_holding_b,
user_holding_lp,
];
let mut groups = SigningGroup::new();
groups
.add_required(a_mention, user_holding_a, self.0)
.and_then(|()| groups.add_required(b_mention, user_holding_b, self.0))
.and_then(|()| groups.add_optional(lp_mention, user_holding_lp, self.0))
.map_err(ExecutionFailureKind::from_anyhow)?;
let instruction = amm_core::Instruction::AddLiquidity {
min_amount_liquidity,
max_amount_to_add_token_a,
max_amount_to_add_token_b,
};
let instruction_data =
Program::serialize_instruction(instruction).expect("Instruction should serialize");
self.0
.send_public_tx(&program, account_ids, instruction, groups)
.send_pub_tx(
vec![
AccountIdentity::PublicNoSign(amm_pool),
AccountIdentity::PublicNoSign(vault_holding_a),
AccountIdentity::PublicNoSign(vault_holding_b),
AccountIdentity::PublicNoSign(pool_lp),
user_holding_a_identity,
user_holding_b_identity,
user_holding_lp_identity,
],
instruction_data,
&program.into(),
)
.await
}
@ -287,16 +369,18 @@ impl Amm<'_> {
remove_liquidity_amount: u128,
min_amount_to_remove_token_a: u128,
min_amount_to_remove_token_b: u128,
lp_mention: &CliAccountMention,
user_holding_lp_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> {
let instruction = amm_core::Instruction::RemoveLiquidity {
remove_liquidity_amount,
min_amount_to_remove_token_a,
min_amount_to_remove_token_b,
};
let user_holding_lp_identity = user_holding_lp_mention.key_path().map_or(
AccountIdentity::Public(user_holding_lp),
|key_path| AccountIdentity::PublicKeycard {
account_id: user_holding_lp,
key_path: key_path.to_owned(),
},
);
let program = Program::amm();
let amm_program_id = Program::amm().id();
let user_a_acc = self
.0
.get_account_public(user_holding_a)
@ -320,23 +404,28 @@ impl Amm<'_> {
let vault_holding_a = compute_vault_pda(amm_program_id, amm_pool, definition_token_a_id);
let vault_holding_b = compute_vault_pda(amm_program_id, amm_pool, definition_token_b_id);
let pool_lp = compute_liquidity_token_pda(amm_program_id, amm_pool);
let instruction = amm_core::Instruction::RemoveLiquidity {
remove_liquidity_amount,
min_amount_to_remove_token_a,
min_amount_to_remove_token_b,
};
let instruction_data =
Program::serialize_instruction(instruction).expect("Instruction should serialize");
let account_ids = vec![
amm_pool,
vault_holding_a,
vault_holding_b,
pool_lp,
user_holding_a,
user_holding_b,
user_holding_lp,
];
let mut groups = SigningGroup::new();
groups
.add_required(lp_mention, user_holding_lp, self.0)
.map_err(ExecutionFailureKind::from_anyhow)?;
self.0
.send_public_tx(&program, account_ids, instruction, groups)
.send_pub_tx(
vec![
AccountIdentity::PublicNoSign(amm_pool),
AccountIdentity::PublicNoSign(vault_holding_a),
AccountIdentity::PublicNoSign(vault_holding_b),
AccountIdentity::PublicNoSign(pool_lp),
AccountIdentity::PublicNoSign(user_holding_a),
AccountIdentity::PublicNoSign(user_holding_b),
user_holding_lp_identity,
],
instruction_data,
&program.into(),
)
.await
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,119 +1,6 @@
use anyhow::Result;
use keycard_wallet::{KeycardWallet, python_path};
use nssa::{AccountId, PrivateKey, PublicKey, Signature};
use pyo3::Python;
use crate::{WalletCore, cli::CliAccountMention};
/// Groups transaction signers by type to minimise Python GIL acquisition.
///
/// Local signers are signed in pure Rust; all keycard signers share a single Python session
/// with one `connect` / `close_session` pair.
#[derive(Default)]
pub struct SigningGroup {
local: Vec<(AccountId, PrivateKey)>,
keycard: Vec<(AccountId, String)>,
}
impl SigningGroup {
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Add a sender. Keycard paths are queued for the hardware session; local accounts
/// have their signing key resolved eagerly. Errors if no key is found.
pub fn add_required(
&mut self,
mention: &CliAccountMention,
account_id: AccountId,
wallet_core: &WalletCore,
) -> Result<()> {
if let CliAccountMention::KeyPath(path) = mention {
self.keycard.push((account_id, path.clone()));
return Ok(());
}
let key = wallet_core
.storage()
.key_chain()
.pub_account_signing_key(account_id)
.ok_or_else(|| anyhow::anyhow!("signing key not found for account {account_id}"))?
.clone();
self.local.push((account_id, key));
Ok(())
}
/// Add a recipient. Same as [`add_required`] but silently skips accounts with no local
/// key and no keycard path — they are foreign and require neither a signature nor a nonce.
pub fn add_optional(
&mut self,
mention: &CliAccountMention,
account_id: AccountId,
wallet_core: &WalletCore,
) -> Result<()> {
if let CliAccountMention::KeyPath(path) = mention {
self.keycard.push((account_id, path.clone()));
return Ok(());
}
if let Some(key) = wallet_core
.storage()
.key_chain()
.pub_account_signing_key(account_id)
{
self.local.push((account_id, key.clone()));
}
Ok(())
}
/// Returns `true` when a PIN is required (at least one keycard signer is present).
#[must_use]
pub const fn needs_pin(&self) -> bool {
!self.keycard.is_empty()
}
/// Account IDs that require a nonce (every non-foreign signer).
#[must_use]
pub fn signing_ids(&self) -> Vec<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.
pub struct KeycardSessionContext {
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`.
/// `None` for regular shared accounts (keys derived from identifier via derivation seed).
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,
}
@ -858,7 +858,7 @@ mod tests {
group_label: Label::new("test-group"),
identifier: 42,
pda_seed: None,
pda_program_id: None,
authority_program_id: None,
account: nssa_core::account::Account::default(),
};
let encoded = bincode::serialize(&entry).expect("serialize");
@ -871,7 +871,7 @@ mod tests {
group_label: Label::new("pda-group"),
identifier: u128::MAX,
pda_seed: Some(PdaSeed::new([7_u8; 32])),
pda_program_id: Some([9; 8]),
authority_program_id: Some([9; 8]),
account: nssa_core::account::Account::default(),
};
let pda_encoded = bincode::serialize(&pda_entry).expect("serialize pda");
@ -890,7 +890,7 @@ mod tests {
group_label: Label::new("old"),
identifier: 1,
pda_seed: None,
pda_program_id: None,
authority_program_id: None,
account: nssa_core::account::Account::default(),
};
let encoded = bincode::serialize(&entry).expect("serialize");