mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 06:01:11 +00:00
test(privacy): add findings doc, stablecoin negative-destination test, refine ATA/token coverage
Adds ata_group_owned_owner_signing (Burn's missing GROUP variant) and two NewFungibleDefinition private-initial-holder tests to Token. Removes two experimental PDA-external-seed Token tests that had no real coverage purpose. Documents the root cause of the AMM privacy-test blocker (a spel-framework guest-wrapper filter silently drops the clock account before either transaction validator sees it) with full findings/matrix writeups, and reconciles table gaps found while cross-checking each program's privacy tests against docs/findings.md.
This commit is contained in:
@@ -3109,7 +3109,8 @@ fn token_program_instance() -> Program {
|
||||
}
|
||||
|
||||
fn twap_oracle_program_instance() -> Program {
|
||||
Program::new(twap_oracle_methods::TWAP_ORACLE_ELF.to_vec().into()).expect("valid twap oracle ELF")
|
||||
Program::new(twap_oracle_methods::TWAP_ORACLE_ELF.to_vec().into())
|
||||
.expect("valid twap oracle ELF")
|
||||
}
|
||||
|
||||
fn amm_with_deps() -> ProgramWithDependencies {
|
||||
@@ -3159,10 +3160,16 @@ fn amm_swap_a_to_b_private_user_holding_is_not_expressible() {
|
||||
false,
|
||||
Ids::pool_definition(),
|
||||
);
|
||||
let vault_a_pre =
|
||||
AccountWithMetadata::new(state.get_account_by_id(Ids::vault_a()), false, Ids::vault_a());
|
||||
let vault_b_pre =
|
||||
AccountWithMetadata::new(state.get_account_by_id(Ids::vault_b()), false, Ids::vault_b());
|
||||
let vault_a_pre = AccountWithMetadata::new(
|
||||
state.get_account_by_id(Ids::vault_a()),
|
||||
false,
|
||||
Ids::vault_a(),
|
||||
);
|
||||
let vault_b_pre = AccountWithMetadata::new(
|
||||
state.get_account_by_id(Ids::vault_b()),
|
||||
false,
|
||||
Ids::vault_b(),
|
||||
);
|
||||
let user_a_pre = AccountWithMetadata::new(user_a_account, true, user_a_id);
|
||||
let user_b_pre =
|
||||
AccountWithMetadata::new(state.get_account_by_id(Ids::user_b()), true, Ids::user_b());
|
||||
@@ -3256,10 +3263,16 @@ fn amm_swap_exact_output_private_user_holding_is_not_expressible() {
|
||||
false,
|
||||
Ids::pool_definition(),
|
||||
);
|
||||
let vault_a_pre =
|
||||
AccountWithMetadata::new(state.get_account_by_id(Ids::vault_a()), false, Ids::vault_a());
|
||||
let vault_b_pre =
|
||||
AccountWithMetadata::new(state.get_account_by_id(Ids::vault_b()), false, Ids::vault_b());
|
||||
let vault_a_pre = AccountWithMetadata::new(
|
||||
state.get_account_by_id(Ids::vault_a()),
|
||||
false,
|
||||
Ids::vault_a(),
|
||||
);
|
||||
let vault_b_pre = AccountWithMetadata::new(
|
||||
state.get_account_by_id(Ids::vault_b()),
|
||||
false,
|
||||
Ids::vault_b(),
|
||||
);
|
||||
let user_a_pre = AccountWithMetadata::new(user_a_account, true, user_a_id);
|
||||
let user_b_pre =
|
||||
AccountWithMetadata::new(state.get_account_by_id(Ids::user_b()), true, Ids::user_b());
|
||||
@@ -3351,10 +3364,16 @@ fn amm_add_liquidity_private_lp_holding_is_not_expressible() {
|
||||
false,
|
||||
Ids::pool_definition(),
|
||||
);
|
||||
let vault_a_pre =
|
||||
AccountWithMetadata::new(state.get_account_by_id(Ids::vault_a()), false, Ids::vault_a());
|
||||
let vault_b_pre =
|
||||
AccountWithMetadata::new(state.get_account_by_id(Ids::vault_b()), false, Ids::vault_b());
|
||||
let vault_a_pre = AccountWithMetadata::new(
|
||||
state.get_account_by_id(Ids::vault_a()),
|
||||
false,
|
||||
Ids::vault_a(),
|
||||
);
|
||||
let vault_b_pre = AccountWithMetadata::new(
|
||||
state.get_account_by_id(Ids::vault_b()),
|
||||
false,
|
||||
Ids::vault_b(),
|
||||
);
|
||||
let token_lp_definition_pre = AccountWithMetadata::new(
|
||||
state.get_account_by_id(Ids::token_lp_definition()),
|
||||
false,
|
||||
@@ -3457,10 +3476,16 @@ fn amm_remove_liquidity_private_lp_holding_is_not_expressible() {
|
||||
false,
|
||||
Ids::pool_definition(),
|
||||
);
|
||||
let vault_a_pre =
|
||||
AccountWithMetadata::new(state.get_account_by_id(Ids::vault_a()), false, Ids::vault_a());
|
||||
let vault_b_pre =
|
||||
AccountWithMetadata::new(state.get_account_by_id(Ids::vault_b()), false, Ids::vault_b());
|
||||
let vault_a_pre = AccountWithMetadata::new(
|
||||
state.get_account_by_id(Ids::vault_a()),
|
||||
false,
|
||||
Ids::vault_a(),
|
||||
);
|
||||
let vault_b_pre = AccountWithMetadata::new(
|
||||
state.get_account_by_id(Ids::vault_b()),
|
||||
false,
|
||||
Ids::vault_b(),
|
||||
);
|
||||
let token_lp_definition_pre = AccountWithMetadata::new(
|
||||
state.get_account_by_id(Ids::token_lp_definition()),
|
||||
false,
|
||||
@@ -3560,10 +3585,16 @@ fn amm_remove_liquidity_private_new_user_holdings_is_not_expressible() {
|
||||
false,
|
||||
Ids::pool_definition(),
|
||||
);
|
||||
let vault_a_pre =
|
||||
AccountWithMetadata::new(state.get_account_by_id(Ids::vault_a()), false, Ids::vault_a());
|
||||
let vault_b_pre =
|
||||
AccountWithMetadata::new(state.get_account_by_id(Ids::vault_b()), false, Ids::vault_b());
|
||||
let vault_a_pre = AccountWithMetadata::new(
|
||||
state.get_account_by_id(Ids::vault_a()),
|
||||
false,
|
||||
Ids::vault_a(),
|
||||
);
|
||||
let vault_b_pre = AccountWithMetadata::new(
|
||||
state.get_account_by_id(Ids::vault_b()),
|
||||
false,
|
||||
Ids::vault_b(),
|
||||
);
|
||||
let token_lp_definition_pre = AccountWithMetadata::new(
|
||||
state.get_account_by_id(Ids::token_lp_definition()),
|
||||
false,
|
||||
@@ -3571,8 +3602,11 @@ fn amm_remove_liquidity_private_new_user_holdings_is_not_expressible() {
|
||||
);
|
||||
let user_a_pre = AccountWithMetadata::new(Account::default(), false, user_a_id);
|
||||
let user_b_pre = AccountWithMetadata::new(Account::default(), false, user_b_id);
|
||||
let user_lp_pre =
|
||||
AccountWithMetadata::new(state.get_account_by_id(Ids::user_lp()), true, Ids::user_lp());
|
||||
let user_lp_pre = AccountWithMetadata::new(
|
||||
state.get_account_by_id(Ids::user_lp()),
|
||||
true,
|
||||
Ids::user_lp(),
|
||||
);
|
||||
let current_tick_pre = AccountWithMetadata::new(
|
||||
state.get_account_by_id(Ids::current_tick_account()),
|
||||
false,
|
||||
@@ -3695,10 +3729,16 @@ fn amm_add_liquidity_private_user_holdings_is_not_expressible() {
|
||||
false,
|
||||
Ids::pool_definition(),
|
||||
);
|
||||
let vault_a_pre =
|
||||
AccountWithMetadata::new(state.get_account_by_id(Ids::vault_a()), false, Ids::vault_a());
|
||||
let vault_b_pre =
|
||||
AccountWithMetadata::new(state.get_account_by_id(Ids::vault_b()), false, Ids::vault_b());
|
||||
let vault_a_pre = AccountWithMetadata::new(
|
||||
state.get_account_by_id(Ids::vault_a()),
|
||||
false,
|
||||
Ids::vault_a(),
|
||||
);
|
||||
let vault_b_pre = AccountWithMetadata::new(
|
||||
state.get_account_by_id(Ids::vault_b()),
|
||||
false,
|
||||
Ids::vault_b(),
|
||||
);
|
||||
let token_lp_definition_pre = AccountWithMetadata::new(
|
||||
state.get_account_by_id(Ids::token_lp_definition()),
|
||||
false,
|
||||
@@ -3706,8 +3746,11 @@ fn amm_add_liquidity_private_user_holdings_is_not_expressible() {
|
||||
);
|
||||
let user_a_pre = AccountWithMetadata::new(user_a_account, true, user_a_id);
|
||||
let user_b_pre = AccountWithMetadata::new(user_b_account, true, user_b_id);
|
||||
let user_lp_pre =
|
||||
AccountWithMetadata::new(state.get_account_by_id(Ids::user_lp()), false, Ids::user_lp());
|
||||
let user_lp_pre = AccountWithMetadata::new(
|
||||
state.get_account_by_id(Ids::user_lp()),
|
||||
false,
|
||||
Ids::user_lp(),
|
||||
);
|
||||
let current_tick_pre = AccountWithMetadata::new(
|
||||
state.get_account_by_id(Ids::current_tick_account()),
|
||||
false,
|
||||
|
||||
@@ -4,10 +4,6 @@ use ata_core::{compute_ata_seed, get_associated_token_account_id};
|
||||
use integration_tests::{
|
||||
private_authorized_init_identity, private_unauthorized_identity, GroupOwner,
|
||||
};
|
||||
use key_protocol::key_management::{
|
||||
group_key_holder::{GroupKeyHolder, SealingPublicKey},
|
||||
secret_holders::SecretSpendingKey,
|
||||
};
|
||||
use nssa::{
|
||||
execute_and_prove,
|
||||
privacy_preserving_transaction::{
|
||||
@@ -968,6 +964,116 @@ fn ata_burn_with_private_owner_signing() {
|
||||
.is_some());
|
||||
}
|
||||
|
||||
/// Group-owned variant of `ata_burn_with_private_owner_signing`: the GMS is distributed through
|
||||
/// the real seal/unseal handshake, and it's Bob — not Alice, who created the group — who
|
||||
/// self-initializes and signs the owner identity in the same transaction via
|
||||
/// `PrivateAuthorizedInit`, then burns from the ATA holding through it.
|
||||
#[test]
|
||||
fn ata_group_owned_owner_signing() {
|
||||
let mut state = V03State::new();
|
||||
deploy_programs(&mut state);
|
||||
state.force_insert_account(Ids::token_definition(), Accounts::token_definition_init());
|
||||
|
||||
let alice = GroupOwner::new([97_u8; 32]);
|
||||
let owner_id = alice.id;
|
||||
let bob_nsk = alice.admit_member();
|
||||
|
||||
// The ATA holding must stay public (per the confirmed PDA finding), so it's seeded
|
||||
// directly rather than via a real `Create` transaction.
|
||||
let seed = compute_ata_seed(Ids::token_program(), owner_id, Ids::token_definition());
|
||||
let ata_id = get_associated_token_account_id(&Ids::ata_program(), &seed);
|
||||
let ata_account = Account {
|
||||
program_owner: Ids::token_program(),
|
||||
balance: 0_u128,
|
||||
data: Data::from(&TokenHolding::Fungible {
|
||||
definition_id: Ids::token_definition(),
|
||||
balance: 1_000_000_u128,
|
||||
}),
|
||||
nonce: Nonce(0),
|
||||
};
|
||||
state.force_insert_account(ata_id, ata_account.clone());
|
||||
|
||||
let owner_pre = AccountWithMetadata::new(Account::default(), true, owner_id);
|
||||
let ata_pre = AccountWithMetadata::new(ata_account, false, ata_id);
|
||||
let def_pre = AccountWithMetadata::new(
|
||||
state.get_account_by_id(Ids::token_definition()),
|
||||
false,
|
||||
Ids::token_definition(),
|
||||
);
|
||||
|
||||
let burn_amount = 300_000_u128;
|
||||
let instruction = ata_core::Instruction::Burn {
|
||||
token_program_id: Ids::token_program(),
|
||||
amount: burn_amount,
|
||||
};
|
||||
|
||||
let ata_program = Program::new(ata_methods::ATA_ELF.to_vec().into()).unwrap();
|
||||
let token_program = Program::new(token_methods::TOKEN_ELF.to_vec().into()).unwrap();
|
||||
let program_with_deps = ProgramWithDependencies::new(
|
||||
ata_program,
|
||||
HashMap::from([(Ids::token_program(), token_program)]),
|
||||
);
|
||||
|
||||
let (output, proof) = execute_and_prove(
|
||||
vec![owner_pre, ata_pre, def_pre],
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
vec![
|
||||
private_authorized_init_identity(bob_nsk, &alice.vpk, 0),
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program_with_deps,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message =
|
||||
Message::try_from_circuit_output(vec![ata_id, Ids::token_definition()], vec![], output)
|
||||
.unwrap();
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(
|
||||
&PrivacyPreservingTransaction::new(message, witness_set),
|
||||
0,
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
state.get_account_by_id(ata_id),
|
||||
Account {
|
||||
program_owner: Ids::token_program(),
|
||||
balance: 0_u128,
|
||||
data: Data::from(&TokenHolding::Fungible {
|
||||
definition_id: Ids::token_definition(),
|
||||
balance: 1_000_000_u128 - burn_amount,
|
||||
}),
|
||||
nonce: Nonce(0),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
state.get_account_by_id(Ids::token_definition()),
|
||||
Account {
|
||||
program_owner: Ids::token_program(),
|
||||
balance: 0_u128,
|
||||
data: Data::from(&TokenDefinition::Fungible {
|
||||
name: String::from("Gold"),
|
||||
total_supply: 1_000_000_u128 - burn_amount,
|
||||
metadata_id: None,
|
||||
authority: None,
|
||||
}),
|
||||
nonce: Nonce(0),
|
||||
}
|
||||
);
|
||||
|
||||
let owner_expected = Account {
|
||||
nonce: Nonce::private_account_nonce_init(&owner_id),
|
||||
..Account::default()
|
||||
};
|
||||
assert!(state
|
||||
.get_proof_for_commitment(&Commitment::new(&owner_id, &owner_expected))
|
||||
.is_some());
|
||||
}
|
||||
|
||||
/// Private owner
|
||||
#[test]
|
||||
fn ata_transfer_with_private_owner_signing() {
|
||||
|
||||
@@ -464,7 +464,6 @@ fn stablecoin_with_token_deps() -> ProgramWithDependencies {
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/// `OpenPosition` is blocked by the `privacy_preserving_circuit` due to the handling of
|
||||
/// sibling chain calls of (uninitialized) private accounts.
|
||||
#[test]
|
||||
@@ -674,7 +673,8 @@ fn stablecoin_withdraw_collateral_private_destination() {
|
||||
|
||||
/// `WithdrawCollateral` blocks withdraws to private accounts (via private donations);
|
||||
/// `PrivateUnauthorized` account initialization (e.g., `nsk` is not known) is not permitted
|
||||
/// due to the assertion in `withdraw_collateral.rs` asserts `destination.account != Account::default()`
|
||||
/// due to the assertion in `withdraw_collateral.rs` asserts `destination.account !=
|
||||
/// Account::default()`
|
||||
#[test]
|
||||
fn stablecoin_withdraw_collateral_to_new_private_destination_is_not_expressible() {
|
||||
let mut state = V03State::new();
|
||||
|
||||
@@ -7,14 +7,12 @@ use nssa::{
|
||||
privacy_preserving_transaction::{Message, PrivacyPreservingTransaction, WitnessSet},
|
||||
program::Program,
|
||||
program_deployment_transaction::{self, ProgramDeploymentTransaction},
|
||||
public_transaction, PrivateKey, PublicKey, PublicTransaction, SharedSecretKey, V03State,
|
||||
public_transaction, PrivateKey, PublicKey, PublicTransaction, V03State,
|
||||
};
|
||||
use nssa_core::{
|
||||
account::{Account, AccountId, AccountWithMetadata, Data, Nonce},
|
||||
encryption::{EphemeralPublicKey, ViewingPublicKey},
|
||||
program::PdaSeed,
|
||||
Commitment, EncryptedAccountData, InputAccountIdentity, Nullifier, NullifierPublicKey,
|
||||
NullifierSecretKey,
|
||||
encryption::ViewingPublicKey,
|
||||
Commitment, InputAccountIdentity, Nullifier, NullifierPublicKey, NullifierSecretKey,
|
||||
};
|
||||
use token_core::{TokenDefinition, TokenHolding};
|
||||
|
||||
@@ -605,75 +603,6 @@ fn token_program() -> Program {
|
||||
Program::new(token_methods::TOKEN_ELF.to_vec().into()).expect("valid token ELF")
|
||||
}
|
||||
|
||||
/// TODO
|
||||
/// EXPERIMENTAL — investigating whether `PrivatePdaInit`'s `seed: Some((seed,
|
||||
/// authority_program_id))` external-derivation-check path lets a private-PDA account be used as an
|
||||
/// input to an *existing* program's flow (Token) without any chained call, `Claim::Pda`, or
|
||||
/// awareness from the `authority_program_id` itself. Per `lee_core`'s
|
||||
/// `circuit_io.rs`/`execution_state.rs`, this path binds the position purely via
|
||||
/// `AccountId::for_private_pda(authority_program_id, seed, npk, identifier) ==
|
||||
/// pre_state.account_id`, checked directly against the top-level `account_identities` — no chained
|
||||
/// call needed. Using `Ids::token_program()` as the `authority_program_id` here, but per the
|
||||
/// circuit source this is not required to correspond to anything Token itself is aware of; it's
|
||||
/// purely a hash input.
|
||||
#[test]
|
||||
fn token_shield_into_private_pda_via_external_seed() {
|
||||
let mut state = state_for_token_tests();
|
||||
let amount = 500_000_u128;
|
||||
|
||||
let sender_id = Ids::holder();
|
||||
let sender_account = state.get_account_by_id(sender_id);
|
||||
let sender_nonce = sender_account.nonce;
|
||||
let sender_pre = AccountWithMetadata::new(sender_account, true, sender_id);
|
||||
|
||||
let authority_program_id = Ids::token_program();
|
||||
let pda_seed = PdaSeed::new([77u8; 32]);
|
||||
let recipient_nsk: NullifierSecretKey = [123u8; 32];
|
||||
let recipient_npk = NullifierPublicKey::from(&recipient_nsk);
|
||||
let recipient_vpk = ViewingPublicKey::from_seed(&[124u8; 32], &[125u8; 32]);
|
||||
let recipient_id =
|
||||
AccountId::for_private_pda(&authority_program_id, &pda_seed, &recipient_npk, 0);
|
||||
|
||||
let recipient_pre = AccountWithMetadata::new(Account::default(), false, recipient_id);
|
||||
|
||||
let shared_secret = SharedSecretKey::encapsulate_deterministic(&recipient_vpk, &[0u8; 32], 0).0;
|
||||
|
||||
let instruction = token_core::Instruction::Transfer {
|
||||
amount_to_transfer: amount,
|
||||
};
|
||||
let (output, proof) = execute_and_prove(
|
||||
vec![sender_pre, recipient_pre],
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&recipient_npk, &recipient_vpk),
|
||||
npk: recipient_npk,
|
||||
ssk: shared_secret,
|
||||
identifier: 0,
|
||||
seed: Some((pda_seed, authority_program_id)),
|
||||
},
|
||||
],
|
||||
&token_program().into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message =
|
||||
Message::try_from_circuit_output(vec![sender_id], vec![sender_nonce], output).unwrap();
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[&Keys::holder_key()]);
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(&tx, 0, 0)
|
||||
.unwrap();
|
||||
|
||||
let recipient_account =
|
||||
Accounts::token_holding(amount, Nonce::private_account_nonce_init(&recipient_id));
|
||||
assert!(state
|
||||
.get_proof_for_commitment(&Commitment::new(&recipient_id, &recipient_account))
|
||||
.is_some());
|
||||
}
|
||||
|
||||
/// Performs a shielded transfer (public → private) of `amount` tokens from
|
||||
/// `Ids::holder()` to a new private account keyed by `PrivateKeys::recipient_*`.
|
||||
/// Returns the resulting private recipient account.
|
||||
@@ -782,7 +711,11 @@ fn token_private_transfer() {
|
||||
shielded_amount,
|
||||
&mut state,
|
||||
false,
|
||||
private_unauthorized_identity(PrivateKeys::recipient_npk(), &PrivateKeys::recipient_vpk(), 0),
|
||||
private_unauthorized_identity(
|
||||
PrivateKeys::recipient_npk(),
|
||||
&PrivateKeys::recipient_vpk(),
|
||||
0,
|
||||
),
|
||||
);
|
||||
let sender_nsk = PrivateKeys::recipient_nsk();
|
||||
let sender_vpk = PrivateKeys::recipient_vpk();
|
||||
@@ -852,7 +785,11 @@ fn token_deshielded_transfer() {
|
||||
shielded_amount,
|
||||
&mut state,
|
||||
false,
|
||||
private_unauthorized_identity(PrivateKeys::recipient_npk(), &PrivateKeys::recipient_vpk(), 0),
|
||||
private_unauthorized_identity(
|
||||
PrivateKeys::recipient_npk(),
|
||||
&PrivateKeys::recipient_vpk(),
|
||||
0,
|
||||
),
|
||||
);
|
||||
let sender_nsk = PrivateKeys::recipient_nsk();
|
||||
let sender_vpk = PrivateKeys::recipient_vpk();
|
||||
@@ -1457,6 +1394,112 @@ fn token_initialize_private_account_without_nsk_is_not_expressible() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token_new_fungible_definition_private_initial_holder() {
|
||||
let mut state = V03State::new();
|
||||
deploy_token(&mut state);
|
||||
|
||||
let holder_nsk = PrivateKeys::holder_nsk();
|
||||
let holder_vpk = PrivateKeys::holder_vpk();
|
||||
let holder_id = PrivateKeys::holder_id();
|
||||
|
||||
let definition_nonce = state.get_account_by_id(Ids::token_definition()).nonce;
|
||||
let definition_pre =
|
||||
AccountWithMetadata::new(Account::default(), true, Ids::token_definition());
|
||||
let holder_pre = AccountWithMetadata::new(Account::default(), true, holder_id);
|
||||
|
||||
let instruction = token_core::Instruction::NewFungibleDefinition {
|
||||
name: String::from("Gold"),
|
||||
total_supply: 1_000_000_u128,
|
||||
mint_authority: None,
|
||||
};
|
||||
let (output, proof) = execute_and_prove(
|
||||
vec![definition_pre, holder_pre],
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
private_authorized_init_identity(holder_nsk, &holder_vpk, 0),
|
||||
],
|
||||
&token_program().into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![Ids::token_definition()],
|
||||
vec![definition_nonce],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[&Keys::def_key()]);
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(&tx, 0, 0)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
state.get_account_by_id(Ids::token_definition()),
|
||||
Account {
|
||||
program_owner: Ids::token_program(),
|
||||
balance: 0_u128,
|
||||
data: Data::from(&TokenDefinition::Fungible {
|
||||
name: String::from("Gold"),
|
||||
total_supply: 1_000_000_u128,
|
||||
metadata_id: None,
|
||||
authority: None,
|
||||
}),
|
||||
nonce: Nonce(1),
|
||||
}
|
||||
);
|
||||
|
||||
let holder_account = Accounts::token_holding(
|
||||
1_000_000_u128,
|
||||
Nonce::private_account_nonce_init(&holder_id),
|
||||
);
|
||||
assert!(state
|
||||
.get_proof_for_commitment(&Commitment::new(&holder_id, &holder_account))
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token_new_fungible_definition_private_holder_without_nsk_is_not_expressible() {
|
||||
let mut state = V03State::new();
|
||||
deploy_token(&mut state);
|
||||
|
||||
let holder_npk = PrivateKeys::holder_npk();
|
||||
let holder_vpk = PrivateKeys::holder_vpk();
|
||||
let holder_id = PrivateKeys::holder_id();
|
||||
|
||||
let definition_pre =
|
||||
AccountWithMetadata::new(Account::default(), true, Ids::token_definition());
|
||||
let holder_pre = AccountWithMetadata::new(Account::default(), false, holder_id);
|
||||
|
||||
let instruction = token_core::Instruction::NewFungibleDefinition {
|
||||
name: String::from("Gold"),
|
||||
total_supply: 1_000_000_u128,
|
||||
mint_authority: None,
|
||||
};
|
||||
let result = execute_and_prove(
|
||||
vec![definition_pre, holder_pre],
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
private_unauthorized_identity(holder_npk, &holder_vpk, 0),
|
||||
],
|
||||
&token_program().into(),
|
||||
);
|
||||
|
||||
let err = result.expect_err(
|
||||
"creating the initial holder via PrivateUnauthorized must be rejected: \
|
||||
NewFungibleDefinition requires is_authorized == true for holding_target_account, but \
|
||||
PrivateUnauthorized forces is_authorized == false",
|
||||
);
|
||||
let message = format!("{err:?}");
|
||||
assert!(
|
||||
message.contains("must be a signer"),
|
||||
"expected the self-service-only rejection, got a different error: {message}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Two independent parties (Alice and Bob) control a private Token holding (via `GroupKeyHolder`).
|
||||
/// Alice initializes the private Token account, and Bob burns tokens from the shared account.
|
||||
#[test]
|
||||
@@ -2102,96 +2145,3 @@ fn token_mint_with_authority_to_private_holding() {
|
||||
.get_proof_for_commitment(&Commitment::new(&recipient_id, &recipient_account))
|
||||
.is_some());
|
||||
}
|
||||
|
||||
/// TODO
|
||||
/// EXPERIMENTAL — follow-up to `token_shield_into_private_pda_via_external_seed`: proves the
|
||||
/// *update* half of the same mechanism (crediting an *existing* private PDA, not just creating
|
||||
/// one), completing a genuine round trip rather than a one-shot creation. `PrivatePdaUpdate`'s
|
||||
/// external seed path has a different pre-condition than `Init`: `execution_state.rs` asserts
|
||||
/// `pre_state.is_authorized ^ external_seed.is_some()` — with an external seed supplied, the
|
||||
/// pre-state must be *unauthorized*, even though we're touching it with a real `nsk` +
|
||||
/// `membership_proof`. That's incompatible with `Transfer`'s sender role, which requires a
|
||||
/// framework-level `#[account(signer)]` (`is_authorized: true`) — confirmed empirically: using
|
||||
/// the private-PDA holder as `Transfer`'s sender fails at the SPEL macro's own validation
|
||||
/// ("must be a signer"), before Token's own logic is ever reached. `Mint`'s
|
||||
/// `user_holding_account` has no such requirement (`mint_inner` never asserts `is_authorized` on
|
||||
/// it, crediting an existing holding or not), so it's used here instead — mirroring the `EXIST`
|
||||
/// dimension's existing-account-crediting pattern (`token_mint_into_existing_private_holding`),
|
||||
/// just with a private-PDA holder instead of a regular private account.
|
||||
#[test]
|
||||
fn token_mint_into_existing_private_pda_via_external_seed() {
|
||||
let mut state = state_for_token_tests_without_recipient();
|
||||
let holding_balance = 500_000_u128;
|
||||
let amount_to_mint = 200_000_u128;
|
||||
|
||||
let authority_program_id = Ids::token_program();
|
||||
let pda_seed = PdaSeed::new([88u8; 32]);
|
||||
let holder_nsk: NullifierSecretKey = [131u8; 32];
|
||||
let holder_npk = NullifierPublicKey::from(&holder_nsk);
|
||||
let holder_vpk = ViewingPublicKey::from_seed(&[132u8; 32], &[133u8; 32]);
|
||||
let holder_id = AccountId::for_private_pda(&authority_program_id, &pda_seed, &holder_npk, 0);
|
||||
|
||||
// Seed the private-PDA holding directly (established technique — no real transaction
|
||||
// needed). Its eligibility as a private PDA is re-derived independently by the update-side
|
||||
// check below; nothing about how it was seeded matters to that check.
|
||||
let holder_account = Accounts::token_holding(
|
||||
holding_balance,
|
||||
Nonce::private_account_nonce_init(&holder_id),
|
||||
);
|
||||
let holder_commitment = Commitment::new(&holder_id, &holder_account);
|
||||
state = state.with_private_accounts([(
|
||||
holder_commitment.clone(),
|
||||
Nullifier::for_account_initialization(&holder_id),
|
||||
)]);
|
||||
let membership_proof = state
|
||||
.get_proof_for_commitment(&holder_commitment)
|
||||
.expect("seeded holder's commitment must be in the set");
|
||||
|
||||
let definition_account = state.get_account_by_id(Ids::token_definition());
|
||||
let definition_nonce = definition_account.nonce;
|
||||
let definition_pre =
|
||||
AccountWithMetadata::new(definition_account, true, Ids::token_definition());
|
||||
let holder_pre = AccountWithMetadata::new(holder_account, false, holder_id);
|
||||
|
||||
let shared_secret = SharedSecretKey::encapsulate_deterministic(&holder_vpk, &[0u8; 32], 0).0;
|
||||
|
||||
let instruction = token_core::Instruction::Mint { amount_to_mint };
|
||||
let (output, proof) = execute_and_prove(
|
||||
vec![definition_pre, holder_pre],
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivatePdaUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&holder_npk, &holder_vpk),
|
||||
ssk: shared_secret,
|
||||
nsk: holder_nsk,
|
||||
membership_proof,
|
||||
identifier: 0,
|
||||
seed: Some((pda_seed, authority_program_id)),
|
||||
},
|
||||
],
|
||||
&token_program().into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![Ids::token_definition()],
|
||||
vec![definition_nonce],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[&Keys::def_key()]);
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(&tx, 0, 0)
|
||||
.unwrap();
|
||||
|
||||
let holder_nonce_after =
|
||||
Nonce::private_account_nonce_init(&holder_id).private_account_nonce_increment(&holder_nsk);
|
||||
let new_holder_account =
|
||||
Accounts::token_holding(holding_balance + amount_to_mint, holder_nonce_after);
|
||||
assert!(state
|
||||
.get_proof_for_commitment(&Commitment::new(&holder_id, &new_holder_account))
|
||||
.is_some());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user