Files
lez-programs/programs/integration_tests/tests/token.rs
T
Marvin JonesandClaude Sonnet 5 bbc8079391 test(privacy): add privacy-preserving test coverage for token and ata programs
Validates the Q2 privacy features (shield/deshield, private-to-private transfers,
existing-account crediting, group-owned accounts, and private PDAs) against the
token and ata program flows, which previously ran almost entirely in public
context. Adds the key_protocol dependency for GMS-based group-account tests and
introduces a docs/privacy-test-matrix.md tracker mapping each program/instruction/
privacy-dimension combination to pass, fail, or not-expressible, with root-cause
findings for each gap (notably that private PDAs are structurally unsupported by
any program currently deriving addresses via for_public_pda).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 15:53:38 -04:00

2246 lines
79 KiB
Rust

use key_protocol::key_management::{
group_key_holder::{GroupKeyHolder, SealingPublicKey},
secret_holders::SecretSpendingKey,
};
use nssa::{
execute_and_prove,
privacy_preserving_transaction::{Message, PrivacyPreservingTransaction, WitnessSet},
program::Program,
program_deployment_transaction::{self, ProgramDeploymentTransaction},
public_transaction, PrivateKey, PublicKey, PublicTransaction, SharedSecretKey, V03State,
};
use nssa_core::{
account::{Account, AccountId, AccountWithMetadata, Data, Nonce},
encryption::{EphemeralPublicKey, ViewingPublicKey},
Commitment, EncryptedAccountData, InputAccountIdentity, Nullifier, NullifierPublicKey,
NullifierSecretKey,
};
use token_core::{TokenDefinition, TokenHolding};
struct Keys;
struct Ids;
struct Accounts;
impl Keys {
fn def_key() -> PrivateKey {
PrivateKey::try_new([10; 32]).expect("valid private key")
}
fn holder_key() -> PrivateKey {
PrivateKey::try_new([11; 32]).expect("valid private key")
}
fn recipient_key() -> PrivateKey {
PrivateKey::try_new([12; 32]).expect("valid private key")
}
fn authority_key() -> PrivateKey {
PrivateKey::try_new([13; 32]).expect("valid private key")
}
}
impl Ids {
fn token_program() -> nssa_core::program::ProgramId {
token_methods::TOKEN_ID
}
fn foreign_token_program() -> nssa_core::program::ProgramId {
[0xfeed_u32; 8]
}
fn token_definition() -> AccountId {
AccountId::from(&PublicKey::new_from_private_key(&Keys::def_key()))
}
fn holder() -> AccountId {
AccountId::from(&PublicKey::new_from_private_key(&Keys::holder_key()))
}
fn recipient() -> AccountId {
AccountId::from(&PublicKey::new_from_private_key(&Keys::recipient_key()))
}
fn authority() -> AccountId {
AccountId::from(&PublicKey::new_from_private_key(&Keys::authority_key()))
}
}
impl Accounts {
fn token_definition_init() -> Account {
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: Some(Ids::token_definition()),
}),
nonce: Nonce(0),
}
}
fn token_definition_foreign_owner() -> Account {
Account {
program_owner: Ids::foreign_token_program(),
balance: 0_u128,
data: Data::from(&TokenDefinition::Fungible {
name: String::from("Gold"),
total_supply: 1_000_000_u128,
metadata_id: None,
authority: Some(Ids::token_definition()),
}),
nonce: Nonce(0),
}
}
fn holder_init() -> 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),
}
}
fn recipient_init() -> Account {
Account {
program_owner: Ids::token_program(),
balance: 0_u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: 0_u128,
}),
nonce: Nonce(0),
}
}
fn authority_init() -> Account {
Account {
program_owner: Ids::token_program(),
balance: 0_u128,
data: Data::default(),
nonce: Nonce(0),
}
}
}
fn deploy_token(state: &mut V03State) {
let message = program_deployment_transaction::Message::new(token_methods::TOKEN_ELF.to_vec());
let tx = ProgramDeploymentTransaction::new(message);
state
.transition_from_program_deployment_transaction(&tx)
.expect("token program deployment must succeed");
}
fn state_for_token_tests() -> V03State {
let mut state = V03State::new();
deploy_token(&mut state);
state.force_insert_account(Ids::token_definition(), Accounts::token_definition_init());
state.force_insert_account(Ids::holder(), Accounts::holder_init());
state.force_insert_account(Ids::recipient(), Accounts::recipient_init());
state.force_insert_account(Ids::authority(), Accounts::authority_init());
state
}
fn state_for_token_tests_without_recipient() -> V03State {
let mut state = V03State::new();
deploy_token(&mut state);
state.force_insert_account(Ids::token_definition(), Accounts::token_definition_init());
state.force_insert_account(Ids::holder(), Accounts::holder_init());
state.force_insert_account(Ids::authority(), Accounts::authority_init());
state
}
#[test]
fn token_new_fungible_definition() {
let mut state = V03State::new();
deploy_token(&mut state);
let instruction = token_core::Instruction::NewFungibleDefinition {
name: String::from("Gold"),
total_supply: 1_000_000_u128,
mint_authority: None,
};
let message = public_transaction::Message::try_new(
Ids::token_program(),
vec![Ids::token_definition(), Ids::holder()],
vec![Nonce(0), Nonce(0)],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(
&message,
&[&Keys::def_key(), &Keys::holder_key()],
);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_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),
}
);
assert_eq!(
state.get_account_by_id(Ids::holder()),
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(1),
}
);
}
#[test]
fn token_initialize_account_succeeds_for_canonical_definition() {
let mut state = state_for_token_tests_without_recipient();
let instruction = token_core::Instruction::InitializeAccount;
let message = public_transaction::Message::try_new(
Ids::token_program(),
vec![Ids::token_definition(), Ids::recipient()],
vec![Nonce(0)],
instruction,
)
.unwrap();
let witness_set =
public_transaction::WitnessSet::for_message(&message, &[&Keys::recipient_key()]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
assert_eq!(
state.get_account_by_id(Ids::token_definition()),
Accounts::token_definition_init()
);
assert_eq!(
state.get_account_by_id(Ids::recipient()),
Account {
program_owner: Ids::token_program(),
balance: 0_u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: 0_u128,
}),
nonce: Nonce(1),
}
);
}
#[test]
fn token_initialize_account_rejects_foreign_owned_definition() {
let mut state = state_for_token_tests_without_recipient();
state.force_insert_account(
Ids::token_definition(),
Accounts::token_definition_foreign_owner(),
);
let instruction = token_core::Instruction::InitializeAccount;
let message = public_transaction::Message::try_new(
Ids::token_program(),
vec![Ids::token_definition(), Ids::recipient()],
vec![Nonce(0)],
instruction,
)
.unwrap();
let witness_set =
public_transaction::WitnessSet::for_message(&message, &[&Keys::recipient_key()]);
let tx = PublicTransaction::new(message, witness_set);
assert!(state.transition_from_public_transaction(&tx, 0, 0).is_err());
assert_eq!(
state.get_account_by_id(Ids::token_definition()),
Accounts::token_definition_foreign_owner()
);
assert_eq!(
state.get_account_by_id(Ids::recipient()),
Account::default()
);
}
#[test]
fn token_transfer() {
let mut state = state_for_token_tests();
let instruction = token_core::Instruction::Transfer {
amount_to_transfer: 500_000_u128,
};
let message = public_transaction::Message::try_new(
Ids::token_program(),
vec![Ids::holder(), Ids::recipient()],
vec![Nonce(0)],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::holder_key()]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
assert_eq!(
state.get_account_by_id(Ids::holder()),
Account {
program_owner: Ids::token_program(),
balance: 0_u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: 500_000_u128,
}),
nonce: Nonce(1),
}
);
assert_eq!(
state.get_account_by_id(Ids::recipient()),
Account {
program_owner: Ids::token_program(),
balance: 0_u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: 500_000_u128,
}),
nonce: Nonce(0),
}
);
}
#[test]
fn token_transfer_fresh_public_recipient_requires_authorization() {
let mut state = state_for_token_tests_without_recipient();
let instruction = token_core::Instruction::Transfer {
amount_to_transfer: 500_000_u128,
};
let message = public_transaction::Message::try_new(
Ids::token_program(),
vec![Ids::holder(), Ids::recipient()],
vec![Nonce(0)],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::holder_key()]);
let tx = PublicTransaction::new(message, witness_set);
assert!(state.transition_from_public_transaction(&tx, 0, 0).is_err());
assert_eq!(
state.get_account_by_id(Ids::holder()),
Accounts::holder_init()
);
assert_eq!(
state.get_account_by_id(Ids::recipient()),
Account::default()
);
}
#[test]
fn token_transfer_fresh_authorized_public_recipient() {
let mut state = state_for_token_tests_without_recipient();
let instruction = token_core::Instruction::Transfer {
amount_to_transfer: 500_000_u128,
};
let message = public_transaction::Message::try_new(
Ids::token_program(),
vec![Ids::holder(), Ids::recipient()],
vec![Nonce(0), Nonce(0)],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(
&message,
&[&Keys::holder_key(), &Keys::recipient_key()],
);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
assert_eq!(
state.get_account_by_id(Ids::holder()),
Account {
program_owner: Ids::token_program(),
balance: 0_u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: 500_000_u128,
}),
nonce: Nonce(1),
}
);
assert_eq!(
state.get_account_by_id(Ids::recipient()),
Account {
program_owner: Ids::token_program(),
balance: 0_u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: 500_000_u128,
}),
nonce: Nonce(1),
}
);
}
#[test]
fn token_burn() {
let mut state = state_for_token_tests();
let instruction = token_core::Instruction::Burn {
amount_to_burn: 200_000_u128,
};
let message = public_transaction::Message::try_new(
Ids::token_program(),
vec![Ids::token_definition(), Ids::holder()],
vec![Nonce(0)],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::holder_key()]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_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: 800_000_u128,
metadata_id: None,
authority: Some(Ids::token_definition()),
}),
nonce: Nonce(0),
}
);
assert_eq!(
state.get_account_by_id(Ids::holder()),
Account {
program_owner: Ids::token_program(),
balance: 0_u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: 800_000_u128,
}),
nonce: Nonce(1),
}
);
}
#[test]
fn token_mint() {
let mut state = state_for_token_tests();
let instruction = token_core::Instruction::Mint {
amount_to_mint: 500_000_u128,
};
let message = public_transaction::Message::try_new(
Ids::token_program(),
vec![Ids::token_definition(), Ids::holder()],
vec![Nonce(0)],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::def_key()]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_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_500_000_u128,
metadata_id: None,
authority: Some(Ids::token_definition()),
}),
nonce: Nonce(1),
}
);
assert_eq!(
state.get_account_by_id(Ids::holder()),
Account {
program_owner: Ids::token_program(),
balance: 0_u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: 1_500_000_u128,
}),
nonce: Nonce(0),
}
);
}
#[test]
fn token_mint_rejects_foreign_owned_definition() {
let mut state = state_for_token_tests_without_recipient();
state.force_insert_account(
Ids::token_definition(),
Accounts::token_definition_foreign_owner(),
);
let instruction = token_core::Instruction::Mint {
amount_to_mint: 500_000_u128,
};
let message = public_transaction::Message::try_new(
Ids::token_program(),
vec![Ids::token_definition(), Ids::recipient()],
vec![Nonce(0), Nonce(0)],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(
&message,
&[&Keys::def_key(), &Keys::recipient_key()],
);
let tx = PublicTransaction::new(message, witness_set);
assert!(state.transition_from_public_transaction(&tx, 0, 0).is_err());
assert_eq!(
state.get_account_by_id(Ids::token_definition()),
Accounts::token_definition_foreign_owner()
);
assert_eq!(
state.get_account_by_id(Ids::recipient()),
Account::default()
);
}
#[test]
fn token_mint_fresh_public_recipient_requires_authorization() {
let mut state = state_for_token_tests_without_recipient();
let instruction = token_core::Instruction::Mint {
amount_to_mint: 500_000_u128,
};
let message = public_transaction::Message::try_new(
Ids::token_program(),
vec![Ids::token_definition(), Ids::recipient()],
vec![Nonce(0)],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::def_key()]);
let tx = PublicTransaction::new(message, witness_set);
assert!(state.transition_from_public_transaction(&tx, 0, 0).is_err());
assert_eq!(
state.get_account_by_id(Ids::token_definition()),
Accounts::token_definition_init()
);
assert_eq!(
state.get_account_by_id(Ids::recipient()),
Account::default()
);
}
#[test]
fn token_mint_fresh_authorized_public_recipient() {
let mut state = state_for_token_tests_without_recipient();
let instruction = token_core::Instruction::Mint {
amount_to_mint: 500_000_u128,
};
let message = public_transaction::Message::try_new(
Ids::token_program(),
vec![Ids::token_definition(), Ids::recipient()],
vec![Nonce(0), Nonce(0)],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(
&message,
&[&Keys::def_key(), &Keys::recipient_key()],
);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_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_500_000_u128,
metadata_id: None,
authority: Some(Ids::token_definition()),
}),
nonce: Nonce(1),
}
);
assert_eq!(
state.get_account_by_id(Ids::recipient()),
Account {
program_owner: Ids::token_program(),
balance: 0_u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: 500_000_u128,
}),
nonce: Nonce(1),
}
);
}
struct PrivateKeys;
impl PrivateKeys {
fn holder_nsk() -> NullifierSecretKey {
[42; 32]
}
fn holder_npk() -> NullifierPublicKey {
NullifierPublicKey::from(&Self::holder_nsk())
}
// `ViewingPublicKey::from_seed` needs two 32-byte halves `(d, z)`. We reuse the
// legacy viewing scalar as `d` and pick a fixed distinct `z`.
fn holder_vpk() -> ViewingPublicKey {
ViewingPublicKey::from_seed(&[73; 32], &[74; 32])
}
fn holder_id() -> AccountId {
AccountId::for_regular_private_account(&Self::holder_npk(), 0)
}
fn recipient_nsk() -> NullifierSecretKey {
[84; 32]
}
fn recipient_npk() -> NullifierPublicKey {
NullifierPublicKey::from(&Self::recipient_nsk())
}
fn recipient_vpk() -> ViewingPublicKey {
ViewingPublicKey::from_seed(&[48; 32], &[49; 32])
}
fn recipient_id() -> AccountId {
AccountId::for_regular_private_account(&Self::recipient_npk(), 0)
}
}
fn token_program() -> Program {
Program::new(token_methods::TOKEN_ELF.to_vec().into()).expect("valid token ELF")
}
/// 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.
#[cfg(test)]
fn shielded_token_transfer(amount: u128, state: &mut V03State) -> Account {
let sender_id = Ids::holder();
let sender_account = state.get_account_by_id(sender_id);
let sender_nonce = sender_account.nonce;
let recipient_npk = PrivateKeys::recipient_npk();
let recipient_vpk = PrivateKeys::recipient_vpk();
let recipient_id = PrivateKeys::recipient_id();
let sender = AccountWithMetadata::new(sender_account, true, sender_id);
let recipient = AccountWithMetadata::new(Account::default(), false, recipient_id);
// Sender encapsulates a shared secret against the recipient's viewing key. The
// circuit fills the real EPK, so we pass an empty placeholder in the identity.
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, recipient],
Program::serialize_instruction(instruction).unwrap(),
vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivateUnauthorized {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&recipient_npk, &recipient_vpk),
npk: recipient_npk,
ssk: shared_secret,
identifier: 0,
},
],
&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();
Account {
program_owner: Ids::token_program(),
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: amount,
}),
nonce: Nonce::private_account_nonce_init(&recipient_id),
}
}
#[test]
fn token_shielded_transfer() {
let mut state = state_for_token_tests();
let amount = 500_000_u128;
let recipient_account = shielded_token_transfer(amount, &mut state);
assert_eq!(
state.get_account_by_id(Ids::holder()),
Account {
program_owner: Ids::token_program(),
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: 1_000_000 - amount,
}),
nonce: Nonce(1),
}
);
let recipient_commitment = Commitment::new(&PrivateKeys::recipient_id(), &recipient_account);
assert!(state
.get_proof_for_commitment(&recipient_commitment)
.is_some());
}
/// Shielded transaction to a private account using the account's `nsk`.
/// `token_shielded_transfer` only uses the account's `npk`; thus, `PrivateUnauthorized` private.
#[test]
fn token_shielded_transfer_authorized_private_init() {
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 recipient_nsk = PrivateKeys::recipient_nsk();
let recipient_npk = PrivateKeys::recipient_npk();
let recipient_vpk = PrivateKeys::recipient_vpk();
let recipient_id = PrivateKeys::recipient_id();
let sender_pre = AccountWithMetadata::new(sender_account, true, sender_id);
let recipient_pre = AccountWithMetadata::new(Account::default(), true, 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::PrivateAuthorizedInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&recipient_npk, &recipient_vpk),
ssk: shared_secret,
nsk: recipient_nsk,
identifier: 0,
},
],
&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();
assert_eq!(
state.get_account_by_id(sender_id),
Account {
program_owner: Ids::token_program(),
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: 1_000_000 - amount,
}),
nonce: Nonce(1),
}
);
let recipient_account = Account {
program_owner: Ids::token_program(),
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: amount,
}),
nonce: Nonce::private_account_nonce_init(&recipient_id),
};
assert!(state
.get_proof_for_commitment(&Commitment::new(&recipient_id, &recipient_account))
.is_some());
}
#[test]
fn token_private_transfer() {
let mut state = state_for_token_tests();
let shielded_amount = 500_000_u128;
let transfer_amount = 200_000_u128;
// Shield tokens into a private account (becomes the sender for the private transfer).
let sender_account = shielded_token_transfer(shielded_amount, &mut state);
let sender_npk = PrivateKeys::recipient_npk();
let sender_nsk = PrivateKeys::recipient_nsk();
let sender_vpk = PrivateKeys::recipient_vpk();
let sender_id = PrivateKeys::recipient_id();
let new_recipient_npk = PrivateKeys::holder_npk();
let new_recipient_vpk = PrivateKeys::holder_vpk();
let new_recipient_id = PrivateKeys::holder_id();
let sender_commitment = Commitment::new(&sender_id, &sender_account);
let membership_proof = state
.get_proof_for_commitment(&sender_commitment)
.expect("sender's commitment must be in the set");
// Distinct `output_index` per private output keeps the encapsulated secrets reproducible.
let shared_secret_1 = SharedSecretKey::encapsulate_deterministic(&sender_vpk, &[0u8; 32], 0).0;
let shared_secret_2 =
SharedSecretKey::encapsulate_deterministic(&new_recipient_vpk, &[0u8; 32], 1).0;
let sender_pre = AccountWithMetadata::new(sender_account.clone(), true, sender_id);
let new_recipient_pre = AccountWithMetadata::new(Account::default(), false, new_recipient_id);
let instruction = token_core::Instruction::Transfer {
amount_to_transfer: transfer_amount,
};
let (output, proof) = execute_and_prove(
vec![sender_pre, new_recipient_pre],
Program::serialize_instruction(instruction).unwrap(),
vec![
InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&sender_npk, &sender_vpk),
ssk: shared_secret_1,
nsk: sender_nsk,
membership_proof,
identifier: 0,
},
InputAccountIdentity::PrivateUnauthorized {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&new_recipient_npk,
&new_recipient_vpk,
),
npk: new_recipient_npk,
ssk: shared_secret_2,
identifier: 0,
},
],
&token_program().into(),
)
.unwrap();
let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[]);
let tx = PrivacyPreservingTransaction::new(message, witness_set);
state
.transition_from_privacy_preserving_transaction(&tx, 0, 0)
.unwrap();
let sender_nonce_after =
Nonce::private_account_nonce_init(&sender_id).private_account_nonce_increment(&sender_nsk);
let new_sender_account = Account {
program_owner: Ids::token_program(),
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: shielded_amount - transfer_amount,
}),
nonce: sender_nonce_after,
};
assert!(state
.get_proof_for_commitment(&Commitment::new(&sender_id, &new_sender_account))
.is_some());
let new_recipient_account = Account {
program_owner: Ids::token_program(),
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: transfer_amount,
}),
nonce: Nonce::private_account_nonce_init(&new_recipient_id),
};
assert!(state
.get_proof_for_commitment(&Commitment::new(&new_recipient_id, &new_recipient_account))
.is_some());
}
#[test]
fn token_deshielded_transfer() {
let mut state = state_for_token_tests();
let shielded_amount = 500_000_u128;
let deshield_amount = 300_000_u128;
// Shield tokens into a private account, then deshield some back to a public account.
let sender_account = shielded_token_transfer(shielded_amount, &mut state);
let sender_npk = PrivateKeys::recipient_npk();
let sender_nsk = PrivateKeys::recipient_nsk();
let sender_vpk = PrivateKeys::recipient_vpk();
let sender_id = PrivateKeys::recipient_id();
let public_recipient_id = Ids::recipient();
let sender_commitment = Commitment::new(&sender_id, &sender_account);
let membership_proof = state
.get_proof_for_commitment(&sender_commitment)
.expect("sender's commitment must be in the set");
let shared_secret = SharedSecretKey::encapsulate_deterministic(&sender_vpk, &[0u8; 32], 0).0;
let public_recipient_pre = AccountWithMetadata::new(
state.get_account_by_id(public_recipient_id),
false,
public_recipient_id,
);
let sender_pre = AccountWithMetadata::new(sender_account.clone(), true, sender_id);
let instruction = token_core::Instruction::Transfer {
amount_to_transfer: deshield_amount,
};
let (output, proof) = execute_and_prove(
vec![sender_pre, public_recipient_pre],
Program::serialize_instruction(instruction).unwrap(),
vec![
InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&sender_npk, &sender_vpk),
ssk: shared_secret,
nsk: sender_nsk,
membership_proof,
identifier: 0,
},
InputAccountIdentity::Public,
],
&token_program().into(),
)
.unwrap();
let message =
Message::try_from_circuit_output(vec![public_recipient_id], vec![], output).unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[]);
let tx = PrivacyPreservingTransaction::new(message, witness_set);
state
.transition_from_privacy_preserving_transaction(&tx, 0, 0)
.unwrap();
assert_eq!(
state.get_account_by_id(public_recipient_id),
Account {
program_owner: Ids::token_program(),
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: deshield_amount,
}),
nonce: Nonce(0),
}
);
let sender_nonce_after =
Nonce::private_account_nonce_init(&sender_id).private_account_nonce_increment(&sender_nsk);
let new_sender_account = Account {
program_owner: Ids::token_program(),
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: shielded_amount - deshield_amount,
}),
nonce: sender_nonce_after,
};
assert!(state
.get_proof_for_commitment(&Commitment::new(&sender_id, &new_sender_account))
.is_some());
}
/// Mints directly to a new recipient private holding (`PrivateUnauthorized`).
/// The recipient's cooperation is unnecessary; only known of the recipient's `npk`, `vpk`.
#[test]
fn token_mint_shielded_to_private_unauthorized() {
let mut state = state_for_token_tests_without_recipient();
let amount_to_mint = 500_000_u128;
let recipient_npk = PrivateKeys::recipient_npk();
let recipient_vpk = PrivateKeys::recipient_vpk();
let recipient_id = PrivateKeys::recipient_id();
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 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::Mint { amount_to_mint };
let (output, proof) = execute_and_prove(
vec![definition_pre, recipient_pre],
Program::serialize_instruction(instruction).unwrap(),
vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivateUnauthorized {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&recipient_npk, &recipient_vpk),
npk: recipient_npk,
ssk: shared_secret,
identifier: 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 + amount_to_mint,
metadata_id: None,
authority: Some(Ids::token_definition()),
}),
nonce: Nonce(1),
}
);
let recipient_account = Account {
program_owner: Ids::token_program(),
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: amount_to_mint,
}),
nonce: Nonce::private_account_nonce_init(&recipient_id),
};
assert!(state
.get_proof_for_commitment(&Commitment::new(&recipient_id, &recipient_account))
.is_some());
}
/// Mints directly to a new recipient private holding (`PrivateAuthorizedInit`).
/// This requires the recipient's secret key `nsk`.
#[test]
fn token_mint_authorized_private_init() {
let mut state = state_for_token_tests_without_recipient();
let amount_to_mint = 500_000_u128;
let recipient_nsk = PrivateKeys::recipient_nsk();
let recipient_npk = PrivateKeys::recipient_npk();
let recipient_vpk = PrivateKeys::recipient_vpk();
let recipient_id = PrivateKeys::recipient_id();
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 recipient_pre = AccountWithMetadata::new(Account::default(), true, recipient_id);
let shared_secret = SharedSecretKey::encapsulate_deterministic(&recipient_vpk, &[0u8; 32], 0).0;
let instruction = token_core::Instruction::Mint { amount_to_mint };
let (output, proof) = execute_and_prove(
vec![definition_pre, recipient_pre],
Program::serialize_instruction(instruction).unwrap(),
vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivateAuthorizedInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&recipient_npk, &recipient_vpk),
ssk: shared_secret,
nsk: recipient_nsk,
identifier: 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 + amount_to_mint,
metadata_id: None,
authority: Some(Ids::token_definition()),
}),
nonce: Nonce(1),
}
);
let recipient_account = Account {
program_owner: Ids::token_program(),
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: amount_to_mint,
}),
nonce: Nonce::private_account_nonce_init(&recipient_id),
};
assert!(state
.get_proof_for_commitment(&Commitment::new(&recipient_id, &recipient_account))
.is_some());
}
/// Mints directly to a pre-existing recipient private holding
/// This requires the recipient's secret key `nsk`.
#[test]
fn token_mint_into_existing_private_holding() {
let mut state = state_for_token_tests_without_recipient();
let pre_balance = 500_000_u128;
let amount_to_mint = 250_000_u128;
let recipient_nsk = PrivateKeys::recipient_nsk();
let recipient_npk = PrivateKeys::recipient_npk();
let recipient_vpk = PrivateKeys::recipient_vpk();
let recipient_id = PrivateKeys::recipient_id();
let recipient_pre = Account {
program_owner: Ids::token_program(),
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: pre_balance,
}),
nonce: Nonce::private_account_nonce_init(&recipient_id),
};
state = state.with_private_accounts([(
Commitment::new(&recipient_id, &recipient_pre),
Nullifier::for_account_initialization(&recipient_id),
)]);
assert!(
state
.get_proof_for_commitment(&Commitment::new(&recipient_id, &recipient_pre))
.is_some(),
"seeded balance must land before the existing-holding mint under test"
);
let membership_proof = state
.get_proof_for_commitment(&Commitment::new(&recipient_id, &recipient_pre))
.expect("recipient'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 existing_recipient_pre =
AccountWithMetadata::new(recipient_pre.clone(), true, recipient_id);
let shared_secret =
SharedSecretKey::encapsulate_deterministic(&recipient_vpk, &[0u8; 32], 0).0;
let (output, second_proof) = execute_and_prove(
vec![definition_pre, existing_recipient_pre],
Program::serialize_instruction(token_core::Instruction::Mint {
amount_to_mint,
})
.unwrap(),
vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&recipient_npk, &recipient_vpk),
ssk: shared_secret,
nsk: recipient_nsk,
membership_proof,
identifier: 0,
},
],
&token_program().into(),
)
.unwrap();
let message = Message::try_from_circuit_output(
vec![Ids::token_definition()],
vec![definition_nonce],
output,
)
.unwrap();
let witness =
WitnessSet::for_message(&message, second_proof, &[&Keys::def_key()]);
state
.transition_from_privacy_preserving_transaction(
&PrivacyPreservingTransaction::new(message, witness),
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 + amount_to_mint,
metadata_id: None,
authority: Some(Ids::token_definition()),
}),
nonce: Nonce(1),
}
);
let recipient_nonce_after = Nonce::private_account_nonce_init(&recipient_id)
.private_account_nonce_increment(&recipient_nsk);
let recipient_after_second_mint = Account {
program_owner: Ids::token_program(),
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: pre_balance + amount_to_mint,
}),
nonce: recipient_nonce_after,
};
assert!(state
.get_proof_for_commitment(&Commitment::new(
&recipient_id,
&recipient_after_second_mint
))
.is_some());
}
/// Burns from an existing private holding (`PrivateAuthorizedUpdate`).
#[test]
fn token_private_burn() {
let mut state = state_for_token_tests();
let holding_balance = 500_000_u128;
let burn_amount = 200_000_u128;
let holder_npk = PrivateKeys::recipient_npk();
let holder_nsk = PrivateKeys::recipient_nsk();
let holder_vpk = PrivateKeys::recipient_vpk();
let holder_id = PrivateKeys::recipient_id();
// Predefined holding account to burn from.
let holder_account = Account {
program_owner: Ids::token_program(),
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: holding_balance,
}),
nonce: 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("holder's commitment must be in the set");
let shared_secret = SharedSecretKey::encapsulate_deterministic(&holder_vpk, &[0u8; 32], 0).0;
let definition_pre = AccountWithMetadata::new(
state.get_account_by_id(Ids::token_definition()),
false,
Ids::token_definition(),
);
let holder_pre = AccountWithMetadata::new(holder_account.clone(), true, holder_id);
let instruction = token_core::Instruction::Burn {
amount_to_burn: burn_amount,
};
let (output, proof) = execute_and_prove(
vec![definition_pre, holder_pre],
Program::serialize_instruction(instruction).unwrap(),
vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&holder_npk, &holder_vpk),
ssk: shared_secret,
nsk: holder_nsk,
membership_proof,
identifier: 0,
},
],
&token_program().into(),
)
.unwrap();
let message =
Message::try_from_circuit_output(vec![Ids::token_definition()], vec![], output).unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[]);
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 - burn_amount,
metadata_id: None,
authority: Some(Ids::token_definition()),
}),
nonce: Nonce(0),
}
);
let new_holder_account = Account {
program_owner: Ids::token_program(),
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: holding_balance - burn_amount,
}),
nonce: Nonce::private_account_nonce_init(&holder_id)
.private_account_nonce_increment(&holder_nsk),
};
assert!(state
.get_proof_for_commitment(&Commitment::new(&holder_id, &new_holder_account))
.is_some());
}
/// Token transfer into a pre-existing Token holding account. This requires
/// the account's `nsk`; `PrivateAuthorizedUpdate`.
#[test]
fn token_transfer_into_existing_private_holding() {
let mut state = state_for_token_tests();
let init_balance = 500_000_u128;
let second_amount = 100_000_u128;
let recipient_npk = PrivateKeys::recipient_npk();
let recipient_nsk = PrivateKeys::recipient_nsk();
let recipient_vpk = PrivateKeys::recipient_vpk();
let recipient_id = PrivateKeys::recipient_id();
let recipient_account = Account {
program_owner: Ids::token_program(),
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: init_balance,
}),
nonce: Nonce::private_account_nonce_init(&recipient_id),
};
let recipient_commitment = Commitment::new(&recipient_id, &recipient_account);
state = state.with_private_accounts([(
recipient_commitment.clone(),
Nullifier::for_account_initialization(&recipient_id),
)]);
let membership_proof = state
.get_proof_for_commitment(&recipient_commitment)
.expect("recipient's commitment must be in the set after seeding");
let sender_id = Ids::holder();
let sender_account = state.get_account_by_id(sender_id);
let sender_nonce = sender_account.nonce;
let shared_secret = SharedSecretKey::encapsulate_deterministic(&recipient_vpk, &[0u8; 32], 0).0;
let sender_pre = AccountWithMetadata::new(sender_account, true, sender_id);
let recipient_pre = AccountWithMetadata::new(recipient_account.clone(), true, recipient_id);
let instruction = token_core::Instruction::Transfer {
amount_to_transfer: second_amount,
};
let (output, proof) = execute_and_prove(
vec![sender_pre, recipient_pre],
Program::serialize_instruction(instruction).unwrap(),
vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&recipient_npk, &recipient_vpk),
ssk: shared_secret,
nsk: recipient_nsk,
membership_proof,
identifier: 0,
},
],
&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();
assert_eq!(
state.get_account_by_id(sender_id),
Account {
program_owner: Ids::token_program(),
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
// `first_amount` was seeded directly into the recipient, never debited from
// the sender — only the real transfer (`second_amount`) actually happened.
balance: 1_000_000 - second_amount,
}),
nonce: Nonce(1),
}
);
let recipient_nonce_after = Nonce::private_account_nonce_init(&recipient_id)
.private_account_nonce_increment(&recipient_nsk);
let new_recipient_account = Account {
program_owner: Ids::token_program(),
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: init_balance + second_amount,
}),
nonce: recipient_nonce_after,
};
assert!(state
.get_proof_for_commitment(&Commitment::new(&recipient_id, &new_recipient_account))
.is_some());
}
// Marvin-todo
/// Fully private counterpart to `token_transfer_into_existing_private_holding`: instead of a
/// *public* sender crediting an existing private recipient, both legs are private and the
/// recipient already exists (not fresh, unlike `token_private_transfer`'s new recipient). This
/// is a new combination — two distinct private accounts, both driven by
/// `PrivateAuthorizedUpdate` (spend + credit-existing) in the same transaction — that neither
/// existing test covers. `Token::Transfer` has no definition-account parameter at all, so with
/// both legs private there is no public account anywhere in this transaction: no signer, no
/// public message ids.
#[test]
fn token_private_transfer_into_existing_private_holding() {
let mut state = state_for_token_tests();
let sender_initial_balance = 500_000_u128;
let recipient_initial_balance = 300_000_u128;
let transfer_amount = 200_000_u128;
let sender_npk = PrivateKeys::recipient_npk();
let sender_nsk = PrivateKeys::recipient_nsk();
let sender_vpk = PrivateKeys::recipient_vpk();
let sender_id = PrivateKeys::recipient_id();
let recipient_npk = PrivateKeys::holder_npk();
let recipient_nsk = PrivateKeys::holder_nsk();
let recipient_vpk = PrivateKeys::holder_vpk();
let recipient_id = PrivateKeys::holder_id();
// Seed both sides directly — neither needs a real prior transaction to exist.
let sender_account = Account {
program_owner: Ids::token_program(),
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: sender_initial_balance,
}),
nonce: Nonce::private_account_nonce_init(&sender_id),
};
let recipient_account = Account {
program_owner: Ids::token_program(),
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: recipient_initial_balance,
}),
nonce: Nonce::private_account_nonce_init(&recipient_id),
};
state = state.with_private_accounts([
(
Commitment::new(&sender_id, &sender_account),
Nullifier::for_account_initialization(&sender_id),
),
(
Commitment::new(&recipient_id, &recipient_account),
Nullifier::for_account_initialization(&recipient_id),
),
]);
let sender_membership_proof = state
.get_proof_for_commitment(&Commitment::new(&sender_id, &sender_account))
.expect("sender's commitment must be in the set");
let recipient_membership_proof = state
.get_proof_for_commitment(&Commitment::new(&recipient_id, &recipient_account))
.expect("recipient's commitment must be in the set");
let sender_shared_secret =
SharedSecretKey::encapsulate_deterministic(&sender_vpk, &[0u8; 32], 0).0;
let recipient_shared_secret =
SharedSecretKey::encapsulate_deterministic(&recipient_vpk, &[0u8; 32], 1).0;
let sender_pre = AccountWithMetadata::new(sender_account.clone(), true, sender_id);
let recipient_pre = AccountWithMetadata::new(recipient_account.clone(), true, recipient_id);
let instruction = token_core::Instruction::Transfer {
amount_to_transfer: transfer_amount,
};
let (output, proof) = execute_and_prove(
vec![sender_pre, recipient_pre],
Program::serialize_instruction(instruction).unwrap(),
vec![
InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&sender_npk, &sender_vpk),
ssk: sender_shared_secret,
nsk: sender_nsk,
membership_proof: sender_membership_proof,
identifier: 0,
},
InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&recipient_npk, &recipient_vpk),
ssk: recipient_shared_secret,
nsk: recipient_nsk,
membership_proof: recipient_membership_proof,
identifier: 0,
},
],
&token_program().into(),
)
.unwrap();
let message = Message::try_from_circuit_output(vec![], 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();
let sender_nonce_after =
Nonce::private_account_nonce_init(&sender_id).private_account_nonce_increment(&sender_nsk);
let new_sender_account = Account {
program_owner: Ids::token_program(),
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: sender_initial_balance - transfer_amount,
}),
nonce: sender_nonce_after,
};
assert!(state
.get_proof_for_commitment(&Commitment::new(&sender_id, &new_sender_account))
.is_some());
let recipient_nonce_after = Nonce::private_account_nonce_init(&recipient_id)
.private_account_nonce_increment(&recipient_nsk);
let new_recipient_account = Account {
program_owner: Ids::token_program(),
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: recipient_initial_balance + transfer_amount,
}),
nonce: recipient_nonce_after,
};
assert!(state
.get_proof_for_commitment(&Commitment::new(&recipient_id, &new_recipient_account))
.is_some());
}
/// Initializes a private holding account directly (private account secret keys are known).
/// `InitializeAccount` requires `account_to_initialize` to be authorized. E.g., for private
/// accounts must be `PrivateAuthorizedInit` and not `PrivateUnauthorized`; the account owner
/// must supply their own `nsk`.
#[test]
fn token_initialize_private_account_succeeds_for_canonical_definition() {
let mut state = state_for_token_tests_without_recipient();
let owner_nsk = PrivateKeys::recipient_nsk();
let owner_npk = PrivateKeys::recipient_npk();
let owner_vpk = PrivateKeys::recipient_vpk();
let owner_id = PrivateKeys::recipient_id();
let definition_pre = AccountWithMetadata::new(
state.get_account_by_id(Ids::token_definition()),
false,
Ids::token_definition(),
);
let account_to_init_pre = AccountWithMetadata::new(Account::default(), true, owner_id);
let shared_secret = SharedSecretKey::encapsulate_deterministic(&owner_vpk, &[0u8; 32], 0).0;
let instruction = token_core::Instruction::InitializeAccount;
let (output, proof) = execute_and_prove(
vec![definition_pre, account_to_init_pre],
Program::serialize_instruction(instruction).unwrap(),
vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivateAuthorizedInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&owner_npk, &owner_vpk),
ssk: shared_secret,
nsk: owner_nsk,
identifier: 0,
},
],
&token_program().into(),
)
.unwrap();
let message =
Message::try_from_circuit_output(vec![Ids::token_definition()], vec![], output).unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[]);
let tx = PrivacyPreservingTransaction::new(message, witness_set);
state
.transition_from_privacy_preserving_transaction(&tx, 0, 0)
.unwrap();
let expected_account = Account {
program_owner: Ids::token_program(),
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: 0,
}),
nonce: Nonce::private_account_nonce_init(&owner_id),
};
assert!(state
.get_proof_for_commitment(&Commitment::new(&owner_id, &expected_account))
.is_some());
}
// TODO: think this is unnecessary; double check.
/// Confirms `InitializeAccount` is self-service-only: unlike `Transfer`/`Mint`, whose recipient
/// host logic never asserts `is_authorized`, the guest's `#[account(init, signer)]` on
/// `account_to_initialize` requires `is_authorized == true` — enforced by the SPEL macro's own
/// account validation before `token_program::initialize::initialize_account`'s host logic
/// (which carries the same assert as defense in depth) ever runs. The only private identity
/// variant satisfying that for a fresh account is `PrivateAuthorizedInit`, which requires
/// supplying `nsk` directly — so a third party cannot initialize a private holding on behalf of
/// an `(npk, vpk, identifier)` whose `nsk` they don't possess. Attempting it via
/// `PrivateUnauthorized` (the variant that *would* allow third-party setup elsewhere) is
/// rejected at the framework's signer check, since that variant forces `is_authorized: false`.
#[test]
fn token_initialize_private_account_without_nsk_is_not_expressible() {
let state = state_for_token_tests_without_recipient();
let recipient_npk = PrivateKeys::recipient_npk();
let recipient_vpk = PrivateKeys::recipient_vpk();
let recipient_id = PrivateKeys::recipient_id();
let definition_pre = AccountWithMetadata::new(
state.get_account_by_id(Ids::token_definition()),
false,
Ids::token_definition(),
);
let account_to_init_pre = AccountWithMetadata::new(Account::default(), false, recipient_id);
let shared_secret = SharedSecretKey::encapsulate_deterministic(&recipient_vpk, &[0u8; 32], 0).0;
let result = execute_and_prove(
vec![definition_pre, account_to_init_pre],
Program::serialize_instruction(token_core::Instruction::InitializeAccount).unwrap(),
vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivateUnauthorized {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&recipient_npk, &recipient_vpk),
npk: recipient_npk,
ssk: shared_secret,
identifier: 0,
},
],
&token_program().into(),
);
let err = result.expect_err(
"initializing a private holding without its nsk must be rejected: InitializeAccount \
requires is_authorized == true, 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 share control of one private Token holding via a `GroupKeyHolder`
/// Group Master Secret (GMS), distributed through the real seal/unseal handshake — not by
/// reusing key material directly — so the test proves actual sharing, not code reuse. "Alice"
/// creates the group and shields tokens into the shared holding; "Bob" only ever receives the
/// *sealed* GMS, independently re-derives the identical nsk/npk from it, and successfully
/// burns from the same holding neither of them personally owns. Validates the `GROUP` Q2
/// dimension: sharing a private account (group-owned) used as a program account.
/// TODO: add a function for spending
#[test]
fn token_group_owned_holding_shared_control_burn() {
let mut state = state_for_token_tests();
let shield_amount = 500_000_u128;
let burn_amount = 200_000_u128;
// Alice creates the group and derives the shared account's keys.
let alice_holder = GroupKeyHolder::new();
let derivation_seed = [7_u8; 32];
let alice_keys = alice_holder.derive_keys_for_shared_account(&derivation_seed);
let group_npk = alice_keys.generate_nullifier_public_key();
let group_vpk = alice_keys.generate_viewing_public_key();
let group_id = AccountId::for_regular_private_account(&group_npk, 0);
// Alice distributes the GMS to Bob via the real seal/unseal handshake, not by handing
// over key material directly.
let bob_sealing_keys = SecretSpendingKey([9_u8; 32]).produce_private_key_holder(None);
let bob_sealing_vpk = bob_sealing_keys.generate_viewing_public_key();
let bob_sealing_vsk = bob_sealing_keys.viewing_secret_key;
let sealed_gms = alice_holder.seal_for(&SealingPublicKey::from_bytes(
bob_sealing_vpk.to_bytes().to_vec(),
));
let bob_holder =
GroupKeyHolder::unseal(&sealed_gms, &bob_sealing_vsk).expect("Bob must unseal the GMS");
// Bob independently re-derives the same shared-account keys from the unsealed GMS.
let bob_keys = bob_holder.derive_keys_for_shared_account(&derivation_seed);
let bob_nsk = bob_keys.nullifier_secret_key;
assert_eq!(
bob_keys.generate_nullifier_public_key(),
group_npk,
"Bob must derive the identical npk as Alice from the shared GMS"
);
// Alice shields tokens into the group-owned holding (mirrors `shielded_token_transfer`,
// parameterized by the group's npk/vpk instead of a personal one).
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 group_pre_shield = AccountWithMetadata::new(Account::default(), false, group_id);
let shield_secret = SharedSecretKey::encapsulate_deterministic(&group_vpk, &[0u8; 32], 0).0;
let shield_instruction = token_core::Instruction::Transfer {
amount_to_transfer: shield_amount,
};
let (shield_output, shield_proof) = execute_and_prove(
vec![sender_pre, group_pre_shield],
Program::serialize_instruction(shield_instruction).unwrap(),
vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivateUnauthorized {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&group_npk, &group_vpk),
npk: group_npk,
ssk: shield_secret,
identifier: 0,
},
],
&token_program().into(),
)
.unwrap();
let shield_message =
Message::try_from_circuit_output(vec![sender_id], vec![sender_nonce], shield_output)
.unwrap();
let shield_witness =
WitnessSet::for_message(&shield_message, shield_proof, &[&Keys::holder_key()]);
let shield_tx = PrivacyPreservingTransaction::new(shield_message, shield_witness);
state
.transition_from_privacy_preserving_transaction(&shield_tx, 0, 0)
.unwrap();
let group_account = Account {
program_owner: Ids::token_program(),
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: shield_amount,
}),
nonce: Nonce::private_account_nonce_init(&group_id),
};
let group_commitment = Commitment::new(&group_id, &group_account);
assert!(state.get_proof_for_commitment(&group_commitment).is_some());
// Bob — who never touched Alice's `GroupKeyHolder` object, only the sealed GMS — burns
// from the group-owned holding using his independently derived nsk.
let membership_proof = state
.get_proof_for_commitment(&group_commitment)
.expect("group holding's commitment must be in the set");
let burn_shared_secret =
SharedSecretKey::encapsulate_deterministic(&group_vpk, &[0u8; 32], 0).0;
let definition_pre = AccountWithMetadata::new(
state.get_account_by_id(Ids::token_definition()),
false,
Ids::token_definition(),
);
let group_pre_burn = AccountWithMetadata::new(group_account, true, group_id);
let burn_instruction = token_core::Instruction::Burn {
amount_to_burn: burn_amount,
};
let (burn_output, burn_proof) = execute_and_prove(
vec![definition_pre, group_pre_burn],
Program::serialize_instruction(burn_instruction).unwrap(),
vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&group_npk, &group_vpk),
ssk: burn_shared_secret,
nsk: bob_nsk,
membership_proof,
identifier: 0,
},
],
&token_program().into(),
)
.unwrap();
let burn_message =
Message::try_from_circuit_output(vec![Ids::token_definition()], vec![], burn_output)
.unwrap();
let burn_witness = WitnessSet::for_message(&burn_message, burn_proof, &[]);
let burn_tx = PrivacyPreservingTransaction::new(burn_message, burn_witness);
state
.transition_from_privacy_preserving_transaction(&burn_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 - burn_amount,
metadata_id: None,
authority: Some(Ids::token_definition()),
}),
nonce: Nonce(0),
}
);
let group_nonce_after =
Nonce::private_account_nonce_init(&group_id).private_account_nonce_increment(&bob_nsk);
let new_group_account = Account {
program_owner: Ids::token_program(),
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: shield_amount - burn_amount,
}),
nonce: group_nonce_after,
};
assert!(state
.get_proof_for_commitment(&Commitment::new(&group_id, &new_group_account))
.is_some());
}
#[test]
fn token_new_fungible_definition_with_authority() {
let mut state = V03State::new();
deploy_token(&mut state);
let authority_key: [u8; 32] = Ids::token_definition()
.as_ref()
.try_into()
.expect("AccountId is always 32 bytes");
let instruction = token_core::Instruction::NewFungibleDefinition {
name: String::from("AuthCoin"),
total_supply: 1_000_000_u128,
mint_authority: Some(AccountId::new(authority_key)),
};
let message = public_transaction::Message::try_new(
Ids::token_program(),
vec![Ids::token_definition(), Ids::holder()],
vec![Nonce(0), Nonce(0)],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(
&message,
&[&Keys::def_key(), &Keys::holder_key()],
);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_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("AuthCoin"),
total_supply: 1_000_000_u128,
metadata_id: None,
authority: Some(AccountId::new(authority_key)),
}),
nonce: Nonce(1),
}
);
}
#[test]
fn token_set_authority_revoke() {
let mut state = V03State::new();
deploy_token(&mut state);
let authority_key: [u8; 32] = Ids::token_definition()
.as_ref()
.try_into()
.expect("AccountId is always 32 bytes");
// Create token with authority
let instruction = token_core::Instruction::NewFungibleDefinition {
name: String::from("AuthCoin"),
total_supply: 1_000_000_u128,
mint_authority: Some(AccountId::new(authority_key)),
};
let message = public_transaction::Message::try_new(
Ids::token_program(),
vec![Ids::token_definition(), Ids::holder()],
vec![Nonce(0), Nonce(0)],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(
&message,
&[&Keys::def_key(), &Keys::holder_key()],
);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
// Seed the authority account so it can sign the revoke
state.force_insert_account(Ids::authority(), Accounts::authority_init());
// Revoke authority
let instruction = token_core::Instruction::SetAuthority {
new_authority: None,
};
let message = public_transaction::Message::try_new(
Ids::token_program(),
vec![Ids::token_definition()],
vec![Nonce(1)],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::def_key()]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_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("AuthCoin"),
total_supply: 1_000_000_u128,
metadata_id: None,
authority: None,
}),
nonce: Nonce(2),
}
);
}
/// After the authority is rotated to an external key, that external key can rotate
/// or revoke again via `SetAuthorityWithAuthority` — signing as a distinct authority
/// account while the definition account does not sign.
#[test]
fn token_set_authority_with_authority_revokes() {
let mut state = V03State::new();
deploy_token(&mut state);
// Create with self-authority (definition is the initial mint authority).
let instruction = token_core::Instruction::NewFungibleDefinition {
name: String::from("RotCoin"),
total_supply: 1_000_000_u128,
mint_authority: Some(Ids::token_definition()),
};
let message = public_transaction::Message::try_new(
Ids::token_program(),
vec![Ids::token_definition(), Ids::holder()],
vec![Nonce(0), Nonce(0)],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(
&message,
&[&Keys::def_key(), &Keys::holder_key()],
);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
// Rotate to the external authority via self-authority (def_key signs).
let instruction = token_core::Instruction::SetAuthority {
new_authority: Some(Ids::authority()),
};
let message = public_transaction::Message::try_new(
Ids::token_program(),
vec![Ids::token_definition()],
vec![Nonce(1)],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::def_key()]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
// Seed the external authority so it can sign.
state.force_insert_account(Ids::authority(), Accounts::authority_init());
// The external authority revokes via SetAuthorityWithAuthority. Accounts:
// [definition, authority]; only the authority signs.
let instruction = token_core::Instruction::SetAuthorityWithAuthority {
new_authority: None,
};
let message = public_transaction::Message::try_new(
Ids::token_program(),
vec![Ids::token_definition(), Ids::authority()],
vec![Nonce(0)],
instruction,
)
.unwrap();
let witness_set =
public_transaction::WitnessSet::for_message(&message, &[&Keys::authority_key()]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
let def = state.get_account_by_id(Ids::token_definition());
let stored = match TokenDefinition::try_from(&def.data).unwrap() {
TokenDefinition::Fungible { authority, .. } => authority,
_ => None,
};
assert_eq!(stored, None, "authority must be permanently revoked");
}
/// Integration test for RFP-001 authority rotation flow:
/// 1. Create a token where `Ids::token_definition()` is the initial mint authority
/// (self-authority).
/// 2. Rotate the mint authority to `Ids::authority()` (an external key).
/// 3. Verify that the new external authority can mint by presenting itself as a rest account.
/// 4. Verify that the OLD authority (def key) can no longer mint after rotation.
#[test]
fn token_rotate_authority_then_new_authority_can_mint() {
let mut state = V03State::new();
deploy_token(&mut state);
let authority_key: [u8; 32] = Ids::authority()
.as_ref()
.try_into()
.expect("AccountId is always 32 bytes");
// Step 1: Create token with self-authority (def account is initial mint authority).
let instruction = token_core::Instruction::NewFungibleDefinition {
name: String::from("RotCoin"),
total_supply: 1_000_000_u128,
mint_authority: Some(AccountId::new(
Ids::token_definition()
.as_ref()
.try_into()
.expect("AccountId is always 32 bytes"),
)),
};
let message = public_transaction::Message::try_new(
Ids::token_program(),
vec![Ids::token_definition(), Ids::holder()],
vec![Nonce(0), Nonce(0)],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(
&message,
&[&Keys::def_key(), &Keys::holder_key()],
);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
// Step 2: Rotate mint authority from def_key to Ids::authority() (external key).
// Self-authority path: no rest accounts; def_key signs.
let instruction = token_core::Instruction::SetAuthority {
new_authority: Some(AccountId::new(authority_key)),
};
let message = public_transaction::Message::try_new(
Ids::token_program(),
vec![Ids::token_definition()],
vec![Nonce(1)],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::def_key()]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
// Verify the authority slot now holds Ids::authority().
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("RotCoin"),
total_supply: 1_000_000_u128,
metadata_id: None,
authority: Some(AccountId::new(authority_key)),
}),
nonce: Nonce(2),
}
);
// Seed the external authority account and the holder so they exist in state.
state.force_insert_account(Ids::authority(), Accounts::authority_init());
state.force_insert_account(Ids::holder(), Accounts::holder_init());
// Step 3: New external authority mints via MintWithAuthority, signing as a
// distinct authority account. Accounts: [definition, holder, authority].
let instruction = token_core::Instruction::MintWithAuthority {
amount_to_mint: 500_000_u128,
};
let message = public_transaction::Message::try_new(
Ids::token_program(),
vec![Ids::token_definition(), Ids::holder(), Ids::authority()],
vec![Nonce(0)],
instruction,
)
.unwrap();
let witness_set =
public_transaction::WitnessSet::for_message(&message, &[&Keys::authority_key()]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
// Verify total_supply increased and holder balance reflects the mint.
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("RotCoin"),
total_supply: 1_500_000_u128,
metadata_id: None,
authority: Some(AccountId::new(authority_key)),
}),
nonce: Nonce(2),
}
);
assert_eq!(
state.get_account_by_id(Ids::holder()),
Account {
program_owner: Ids::token_program(),
balance: 0_u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_definition(),
balance: 1_500_000_u128,
}),
nonce: Nonce(0),
}
);
// Step 4: OLD authority (def_key self-authority path) must be rejected after rotation.
let instruction = token_core::Instruction::Mint {
amount_to_mint: 1_u128,
};
let message = public_transaction::Message::try_new(
Ids::token_program(),
vec![Ids::token_definition(), Ids::holder()],
vec![Nonce(0)],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::def_key()]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx, 0, 0);
assert!(
result.is_err(),
"Old authority must be rejected after rotation"
);
}