mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 14:11:09 +00:00
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>
1133 lines
41 KiB
Rust
1133 lines
41 KiB
Rust
use std::collections::HashMap;
|
|
|
|
use ata_core::{compute_ata_seed, get_associated_token_account_id};
|
|
use key_protocol::key_management::{
|
|
group_key_holder::{GroupKeyHolder, SealingPublicKey},
|
|
secret_holders::SecretSpendingKey,
|
|
};
|
|
use nssa::{
|
|
execute_and_prove,
|
|
privacy_preserving_transaction::{
|
|
circuit::ProgramWithDependencies, 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, 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 owner_key() -> PrivateKey {
|
|
PrivateKey::try_new([11; 32]).expect("valid private key")
|
|
}
|
|
|
|
fn recipient_key() -> PrivateKey {
|
|
PrivateKey::try_new([12; 32]).expect("valid private key")
|
|
}
|
|
}
|
|
|
|
impl Ids {
|
|
fn token_program() -> nssa_core::program::ProgramId {
|
|
token_methods::TOKEN_ID
|
|
}
|
|
|
|
fn ata_program() -> nssa_core::program::ProgramId {
|
|
ata_methods::ATA_ID
|
|
}
|
|
|
|
fn token_definition() -> AccountId {
|
|
AccountId::from(&PublicKey::new_from_private_key(&Keys::def_key()))
|
|
}
|
|
|
|
fn owner() -> AccountId {
|
|
AccountId::from(&PublicKey::new_from_private_key(&Keys::owner_key()))
|
|
}
|
|
|
|
fn recipient() -> AccountId {
|
|
AccountId::from(&PublicKey::new_from_private_key(&Keys::recipient_key()))
|
|
}
|
|
|
|
fn owner_ata() -> AccountId {
|
|
let seed = compute_ata_seed(
|
|
Self::token_program(),
|
|
Self::owner(),
|
|
Self::token_definition(),
|
|
);
|
|
get_associated_token_account_id(&Self::ata_program(), &seed)
|
|
}
|
|
|
|
fn recipient_ata() -> AccountId {
|
|
let seed = compute_ata_seed(
|
|
Self::token_program(),
|
|
Self::recipient(),
|
|
Self::token_definition(),
|
|
);
|
|
get_associated_token_account_id(&Self::ata_program(), &seed)
|
|
}
|
|
}
|
|
|
|
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: None,
|
|
}),
|
|
nonce: Nonce(0),
|
|
}
|
|
}
|
|
|
|
fn owner_ata_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_ata_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 foreign_owned_token_definition() -> Account {
|
|
Account {
|
|
program_owner: [99; 8],
|
|
balance: 0_u128,
|
|
data: Data::from(&TokenDefinition::Fungible {
|
|
name: String::from("Foreign Gold"),
|
|
total_supply: 1_000_000_u128,
|
|
metadata_id: None,
|
|
authority: None,
|
|
}),
|
|
nonce: Nonce(0),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn deploy_programs(state: &mut V03State) {
|
|
let token_message =
|
|
program_deployment_transaction::Message::new(token_methods::TOKEN_ELF.to_vec());
|
|
state
|
|
.transition_from_program_deployment_transaction(&ProgramDeploymentTransaction::new(
|
|
token_message,
|
|
))
|
|
.expect("token program deployment must succeed");
|
|
|
|
let ata_message = program_deployment_transaction::Message::new(ata_methods::ATA_ELF.to_vec());
|
|
state
|
|
.transition_from_program_deployment_transaction(&ProgramDeploymentTransaction::new(
|
|
ata_message,
|
|
))
|
|
.expect("ata program deployment must succeed");
|
|
}
|
|
|
|
fn state_for_ata_tests() -> V03State {
|
|
let mut state = V03State::new();
|
|
deploy_programs(&mut state);
|
|
state.force_insert_account(Ids::token_definition(), Accounts::token_definition_init());
|
|
state.force_insert_account(Ids::owner_ata(), Accounts::owner_ata_init());
|
|
state
|
|
}
|
|
|
|
fn state_for_ata_tests_with_precreated_recipient_ata() -> V03State {
|
|
let mut state = state_for_ata_tests();
|
|
state.force_insert_account(Ids::recipient_ata(), Accounts::recipient_ata_init());
|
|
state
|
|
}
|
|
|
|
#[test]
|
|
fn ata_create() {
|
|
let mut state = V03State::new();
|
|
deploy_programs(&mut state);
|
|
state.force_insert_account(Ids::token_definition(), Accounts::token_definition_init());
|
|
|
|
let instruction = ata_core::Instruction::Create {
|
|
token_program_id: Ids::token_program(),
|
|
};
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
Ids::ata_program(),
|
|
vec![Ids::owner(), Ids::token_definition(), Ids::owner_ata()],
|
|
vec![Nonce(0)],
|
|
instruction,
|
|
)
|
|
.unwrap();
|
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::owner_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::owner_ata()),
|
|
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),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ata_create_is_idempotent() {
|
|
let mut state = state_for_ata_tests();
|
|
|
|
let instruction = ata_core::Instruction::Create {
|
|
token_program_id: Ids::token_program(),
|
|
};
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
Ids::ata_program(),
|
|
vec![Ids::owner(), Ids::token_definition(), Ids::owner_ata()],
|
|
vec![Nonce(0)],
|
|
instruction,
|
|
)
|
|
.unwrap();
|
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::owner_key()]);
|
|
|
|
let tx = PublicTransaction::new(message, witness_set);
|
|
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
|
|
|
|
// Already initialized — should remain unchanged
|
|
assert_eq!(
|
|
state.get_account_by_id(Ids::owner_ata()),
|
|
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),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ata_create_rejects_definition_owned_by_unexpected_token_program() {
|
|
let mut state = V03State::new();
|
|
deploy_programs(&mut state);
|
|
state.force_insert_account(
|
|
Ids::token_definition(),
|
|
Accounts::foreign_owned_token_definition(),
|
|
);
|
|
|
|
let instruction = ata_core::Instruction::Create {
|
|
token_program_id: Ids::token_program(),
|
|
};
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
Ids::ata_program(),
|
|
vec![Ids::owner(), Ids::token_definition(), Ids::owner_ata()],
|
|
vec![Nonce(0)],
|
|
instruction,
|
|
)
|
|
.unwrap();
|
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::owner_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::owner_ata()),
|
|
Account::default()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ata_create_rejects_existing_ata_owned_by_unexpected_token_program() {
|
|
let mut state = V03State::new();
|
|
deploy_programs(&mut state);
|
|
state.force_insert_account(Ids::token_definition(), Accounts::token_definition_init());
|
|
|
|
let mut foreign_ata = Accounts::owner_ata_init();
|
|
foreign_ata.program_owner = [99; 8];
|
|
state.force_insert_account(Ids::owner_ata(), foreign_ata.clone());
|
|
|
|
let instruction = ata_core::Instruction::Create {
|
|
token_program_id: Ids::token_program(),
|
|
};
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
Ids::ata_program(),
|
|
vec![Ids::owner(), Ids::token_definition(), Ids::owner_ata()],
|
|
vec![Nonce(0)],
|
|
instruction,
|
|
)
|
|
.unwrap();
|
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::owner_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::owner_ata()), foreign_ata);
|
|
}
|
|
|
|
#[test]
|
|
fn ata_create_rejects_existing_ata_with_mismatched_definition() {
|
|
let mut state = V03State::new();
|
|
deploy_programs(&mut state);
|
|
state.force_insert_account(Ids::token_definition(), Accounts::token_definition_init());
|
|
|
|
let mut mismatched_ata = Accounts::owner_ata_init();
|
|
mismatched_ata.data = Data::from(&TokenHolding::Fungible {
|
|
definition_id: Ids::recipient(),
|
|
balance: 1_000_000_u128,
|
|
});
|
|
state.force_insert_account(Ids::owner_ata(), mismatched_ata.clone());
|
|
|
|
let instruction = ata_core::Instruction::Create {
|
|
token_program_id: Ids::token_program(),
|
|
};
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
Ids::ata_program(),
|
|
vec![Ids::owner(), Ids::token_definition(), Ids::owner_ata()],
|
|
vec![Nonce(0)],
|
|
instruction,
|
|
)
|
|
.unwrap();
|
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::owner_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::owner_ata()), mismatched_ata);
|
|
}
|
|
|
|
#[test]
|
|
fn ata_transfer() {
|
|
let mut state = state_for_ata_tests_with_precreated_recipient_ata();
|
|
|
|
let instruction = ata_core::Instruction::Transfer {
|
|
token_program_id: Ids::token_program(),
|
|
amount: 400_000_u128,
|
|
};
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
Ids::ata_program(),
|
|
vec![Ids::owner(), Ids::owner_ata(), Ids::recipient_ata()],
|
|
vec![Nonce(0)],
|
|
instruction,
|
|
)
|
|
.unwrap();
|
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::owner_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::owner_ata()),
|
|
Account {
|
|
program_owner: Ids::token_program(),
|
|
balance: 0_u128,
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
definition_id: Ids::token_definition(),
|
|
balance: 600_000_u128,
|
|
}),
|
|
nonce: Nonce(0),
|
|
}
|
|
);
|
|
|
|
assert_eq!(
|
|
state.get_account_by_id(Ids::recipient_ata()),
|
|
Account {
|
|
program_owner: Ids::token_program(),
|
|
balance: 0_u128,
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
definition_id: Ids::token_definition(),
|
|
balance: 400_000_u128,
|
|
}),
|
|
nonce: Nonce(0),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ata_transfer_rejects_default_recipient() {
|
|
let mut state = state_for_ata_tests();
|
|
|
|
let instruction = ata_core::Instruction::Transfer {
|
|
token_program_id: Ids::token_program(),
|
|
amount: 1_u128,
|
|
};
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
Ids::ata_program(),
|
|
vec![Ids::owner(), Ids::owner_ata(), Ids::recipient_ata()],
|
|
vec![Nonce(0)],
|
|
instruction,
|
|
)
|
|
.unwrap();
|
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::owner_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::owner_ata()),
|
|
Accounts::owner_ata_init()
|
|
);
|
|
assert_eq!(
|
|
state.get_account_by_id(Ids::recipient_ata()),
|
|
Account::default()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ata_transfer_rejects_mismatched_definition_recipient() {
|
|
let mut state = state_for_ata_tests_with_precreated_recipient_ata();
|
|
|
|
// Replace the recipient ATA with a token holding pointing at a different definition.
|
|
let foreign_definition_id = AccountId::from(&PublicKey::new_from_private_key(
|
|
&PrivateKey::try_new([42; 32]).expect("valid private key"),
|
|
));
|
|
let mismatched_recipient = Account {
|
|
program_owner: Ids::token_program(),
|
|
balance: 0_u128,
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
definition_id: foreign_definition_id,
|
|
balance: 0_u128,
|
|
}),
|
|
nonce: Nonce(0),
|
|
};
|
|
state.force_insert_account(Ids::recipient_ata(), mismatched_recipient.clone());
|
|
|
|
let instruction = ata_core::Instruction::Transfer {
|
|
token_program_id: Ids::token_program(),
|
|
amount: 1_u128,
|
|
};
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
Ids::ata_program(),
|
|
vec![Ids::owner(), Ids::owner_ata(), Ids::recipient_ata()],
|
|
vec![Nonce(0)],
|
|
instruction,
|
|
)
|
|
.unwrap();
|
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::owner_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::owner_ata()),
|
|
Accounts::owner_ata_init()
|
|
);
|
|
assert_eq!(
|
|
state.get_account_by_id(Ids::recipient_ata()),
|
|
mismatched_recipient
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ata_burn() {
|
|
let mut state = state_for_ata_tests();
|
|
|
|
let instruction = ata_core::Instruction::Burn {
|
|
token_program_id: Ids::token_program(),
|
|
amount: 300_000_u128,
|
|
};
|
|
|
|
let message = public_transaction::Message::try_new(
|
|
Ids::ata_program(),
|
|
vec![Ids::owner(), Ids::owner_ata(), Ids::token_definition()],
|
|
vec![Nonce(0)],
|
|
instruction,
|
|
)
|
|
.unwrap();
|
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::owner_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::owner_ata()),
|
|
Account {
|
|
program_owner: Ids::token_program(),
|
|
balance: 0_u128,
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
definition_id: Ids::token_definition(),
|
|
balance: 700_000_u128,
|
|
}),
|
|
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: 700_000_u128,
|
|
metadata_id: None,
|
|
authority: None,
|
|
}),
|
|
nonce: Nonce(0),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ata_create_from_private_owner() {
|
|
let mut state = V03State::new();
|
|
deploy_programs(&mut state);
|
|
state.force_insert_account(Ids::token_definition(), Accounts::token_definition_init());
|
|
|
|
// Private owner key material
|
|
let owner_nsk: NullifierSecretKey = [13u8; 32];
|
|
let owner_npk = NullifierPublicKey::from(&owner_nsk);
|
|
// `ViewingPublicKey::from_seed` needs two 32-byte halves `(d, z)`.
|
|
let owner_vpk = ViewingPublicKey::from_seed(&[31u8; 32], &[32u8; 32]);
|
|
let owner_id = AccountId::for_regular_private_account(&owner_npk, 0);
|
|
|
|
// ATA derived from the private owner
|
|
let seed = compute_ata_seed(Ids::token_program(), owner_id, Ids::token_definition());
|
|
let owner_ata_id = get_associated_token_account_id(&Ids::ata_program(), &seed);
|
|
|
|
// Pre-states: private uninitialized owner, public token definition, public uninitialized ATA.
|
|
let owner_pre = AccountWithMetadata::new(Account::default(), false, owner_id);
|
|
let def_pre = AccountWithMetadata::new(
|
|
Accounts::token_definition_init(),
|
|
false,
|
|
Ids::token_definition(),
|
|
);
|
|
let ata_pre = AccountWithMetadata::new(Account::default(), false, owner_ata_id);
|
|
|
|
let instruction = ata_core::Instruction::Create {
|
|
token_program_id: Ids::token_program(),
|
|
};
|
|
let instruction_data = Program::serialize_instruction(instruction).unwrap();
|
|
|
|
// Encapsulate a shared secret against the owner's viewing key; the circuit fills the EPK.
|
|
let shared_secret = SharedSecretKey::encapsulate_deterministic(&owner_vpk, &[0u8; 32], 0).0;
|
|
|
|
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, def_pre, ata_pre],
|
|
instruction_data,
|
|
vec![
|
|
// owner: new private account, not owned/spent by the caller (no nsk, no proof).
|
|
InputAccountIdentity::PrivateUnauthorized {
|
|
epk: EphemeralPublicKey(Vec::new()),
|
|
view_tag: EncryptedAccountData::compute_view_tag(&owner_npk, &owner_vpk),
|
|
npk: owner_npk,
|
|
ssk: shared_secret,
|
|
identifier: 0,
|
|
},
|
|
// token_definition: public
|
|
InputAccountIdentity::Public,
|
|
// ata: public
|
|
InputAccountIdentity::Public,
|
|
],
|
|
&program_with_deps,
|
|
)
|
|
.unwrap();
|
|
|
|
let message = Message::try_from_circuit_output(
|
|
vec![Ids::token_definition(), owner_ata_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(owner_ata_id),
|
|
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),
|
|
}
|
|
);
|
|
}
|
|
|
|
// Marvin-todo
|
|
/// Documents a confirmed protocol gap (`PDA` Q2 dimension): the ATA holding can never be made
|
|
/// a private account as ATA is currently coded. `Create`'s `ChainedCall.pda_seeds` authorizes
|
|
/// Token to mutate `for_public_pda(ata_program_id, seed)` — a *public*-form PDA match. Per
|
|
/// `resolve_authorization_and_record_bindings` in `lee_core`'s `execution_state.rs`, a
|
|
/// caller-seed match only gets recorded in `private_pda_bound_positions` when it matches under
|
|
/// `for_private_pda` (`is_private_form == true`); a public-form match authorizes the account
|
|
/// but never binds it as a private PDA. Since `PrivatePdaInit`/`PrivatePdaUpdate` require their
|
|
/// position to appear in that binding map (`execution_state.rs:211`), and ATA's own
|
|
/// `verify_ata_and_get_seed` independently requires the account id to equal
|
|
/// `for_public_pda(ata_program_id, seed)` (never `for_private_pda`'s output, by construction),
|
|
/// these two requirements can never both hold for the same account_id. This is not
|
|
/// program-specific friction — it's structural: fixing it would require `ata_core` (and
|
|
/// equally amm_core / stablecoin_core) to derive their PDAs via `for_private_pda` instead,
|
|
/// which is a source change to the program, not a test workaround.
|
|
#[test]
|
|
fn ata_create_private_ata_holding_is_not_expressible() {
|
|
let mut state = V03State::new();
|
|
deploy_programs(&mut state);
|
|
state.force_insert_account(Ids::token_definition(), Accounts::token_definition_init());
|
|
|
|
let owner_id = Ids::owner();
|
|
let owner_account = state.get_account_by_id(owner_id);
|
|
|
|
// Fresh personal npk/vpk for the ATA holding's privacy identity — distinct from the
|
|
// owner's plain public keypair, which only supplies the seed input.
|
|
let ata_nsk: NullifierSecretKey = [21u8; 32];
|
|
let ata_npk = NullifierPublicKey::from(&ata_nsk);
|
|
let ata_vpk = ViewingPublicKey::from_seed(&[41u8; 32], &[42u8; 32]);
|
|
|
|
// Address stays the *standard* public-PDA formula ATA always uses — only its state
|
|
// privacy is under test, not its address derivation.
|
|
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 owner_pre = AccountWithMetadata::new(owner_account, true, owner_id);
|
|
let def_pre = AccountWithMetadata::new(
|
|
Accounts::token_definition_init(),
|
|
false,
|
|
Ids::token_definition(),
|
|
);
|
|
let ata_pre = AccountWithMetadata::new(Account::default(), false, ata_id);
|
|
|
|
let instruction = ata_core::Instruction::Create {
|
|
token_program_id: Ids::token_program(),
|
|
};
|
|
let instruction_data = Program::serialize_instruction(instruction).unwrap();
|
|
|
|
let shared_secret = SharedSecretKey::encapsulate_deterministic(&ata_vpk, &[0u8; 32], 0).0;
|
|
|
|
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 result = execute_and_prove(
|
|
vec![owner_pre, def_pre, ata_pre],
|
|
instruction_data,
|
|
vec![
|
|
InputAccountIdentity::Public,
|
|
InputAccountIdentity::Public,
|
|
InputAccountIdentity::PrivatePdaInit {
|
|
epk: EphemeralPublicKey(Vec::new()),
|
|
view_tag: EncryptedAccountData::compute_view_tag(&ata_npk, &ata_vpk),
|
|
npk: ata_npk,
|
|
ssk: shared_secret,
|
|
identifier: 0,
|
|
seed: None,
|
|
},
|
|
],
|
|
&program_with_deps,
|
|
);
|
|
|
|
let err = result.expect_err(
|
|
"a private-PDA ATA holding must be rejected: its account id can never satisfy both \
|
|
ATA's own for_public_pda address check and PrivatePdaInit's for_private_pda binding \
|
|
requirement simultaneously",
|
|
);
|
|
let message = format!("{err:?}");
|
|
assert!(
|
|
message.contains("has no proven (seed, npk) binding via Claim::Pda or caller pda_seeds"),
|
|
"expected the private-PDA binding rejection, got a different error: {message}"
|
|
);
|
|
}
|
|
|
|
// Marvin-todo
|
|
/// Credits an *already-existing* private holding through ATA's chained call to Token, and
|
|
/// documents a structural finding along the way:
|
|
/// `ata_program::transfer::transfer_from_associated_token_account` hard-asserts `recipient.account
|
|
/// != Account::default()` ("Recipient token holding must be initialized"), so a *fresh* private
|
|
/// recipient (shield-style, `PrivateUnauthorized`) can never be created through `ATA::Transfer` —
|
|
/// only an existing account can be credited. That collapses what would otherwise be separate `BASE`
|
|
/// and `EXIST` tests into one: this test necessarily exercises both "private account through a
|
|
/// chained call" (`CHAIN`) and "sending to an existing private account" (`EXIST`, requiring the
|
|
/// recipient's cooperation via `PrivateAuthorizedUpdate`, per the finding already confirmed in
|
|
/// `token.rs`).
|
|
///
|
|
/// The private holding is funded beforehand via a direct (non-ATA) `Token::Transfer` shield
|
|
/// from a throwaway public holder, since neither `ATA::Transfer` (blocked by the assert above)
|
|
/// nor `Token::Mint` (this test fixture's definition has `authority: None`, fixed supply) can
|
|
/// create it.
|
|
#[test]
|
|
fn ata_transfer_to_existing_private_recipient() {
|
|
let mut state = state_for_ata_tests();
|
|
|
|
// A throwaway public holder to shield from directly via Token — bypassing ATA entirely,
|
|
// since ATA::Transfer cannot originate a fresh private recipient (see doc comment above).
|
|
let shield_source_key = PrivateKey::try_new([77u8; 32]).expect("valid private key");
|
|
let shield_source_id = AccountId::from(&PublicKey::new_from_private_key(&shield_source_key));
|
|
state.force_insert_account(
|
|
shield_source_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,
|
|
}),
|
|
nonce: Nonce(0),
|
|
},
|
|
);
|
|
|
|
let recipient_nsk: NullifierSecretKey = [51u8; 32];
|
|
let recipient_npk = NullifierPublicKey::from(&recipient_nsk);
|
|
let recipient_vpk = ViewingPublicKey::from_seed(&[61u8; 32], &[62u8; 32]);
|
|
let recipient_id = AccountId::for_regular_private_account(&recipient_npk, 0);
|
|
|
|
let shield_amount = 500_000_u128;
|
|
let source_pre = AccountWithMetadata::new(
|
|
state.get_account_by_id(shield_source_id),
|
|
true,
|
|
shield_source_id,
|
|
);
|
|
let fresh_recipient_pre = AccountWithMetadata::new(Account::default(), false, recipient_id);
|
|
let shield_secret = SharedSecretKey::encapsulate_deterministic(&recipient_vpk, &[0u8; 32], 0).0;
|
|
|
|
let token_program_for_shield = Program::new(token_methods::TOKEN_ELF.to_vec().into())
|
|
.expect("valid token ELF")
|
|
.into();
|
|
let (shield_output, shield_proof) = execute_and_prove(
|
|
vec![source_pre, fresh_recipient_pre],
|
|
Program::serialize_instruction(token_core::Instruction::Transfer {
|
|
amount_to_transfer: shield_amount,
|
|
})
|
|
.unwrap(),
|
|
vec![
|
|
InputAccountIdentity::Public,
|
|
InputAccountIdentity::PrivateUnauthorized {
|
|
epk: EphemeralPublicKey(Vec::new()),
|
|
view_tag: EncryptedAccountData::compute_view_tag(&recipient_npk, &recipient_vpk),
|
|
npk: recipient_npk,
|
|
ssk: shield_secret,
|
|
identifier: 0,
|
|
},
|
|
],
|
|
&token_program_for_shield,
|
|
)
|
|
.unwrap();
|
|
let shield_message =
|
|
Message::try_from_circuit_output(vec![shield_source_id], vec![Nonce(0)], shield_output)
|
|
.unwrap();
|
|
let shield_witness =
|
|
WitnessSet::for_message(&shield_message, shield_proof, &[&shield_source_key]);
|
|
state
|
|
.transition_from_privacy_preserving_transaction(
|
|
&PrivacyPreservingTransaction::new(shield_message, shield_witness),
|
|
0,
|
|
0,
|
|
)
|
|
.unwrap();
|
|
|
|
let recipient_after_shield = Account {
|
|
program_owner: Ids::token_program(),
|
|
balance: 0_u128,
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
definition_id: Ids::token_definition(),
|
|
balance: shield_amount,
|
|
}),
|
|
nonce: Nonce::private_account_nonce_init(&recipient_id),
|
|
};
|
|
assert!(
|
|
state
|
|
.get_proof_for_commitment(&Commitment::new(&recipient_id, &recipient_after_shield))
|
|
.is_some(),
|
|
"shield setup must land before the ATA transfer under test"
|
|
);
|
|
|
|
// Now the actual test: owner's ATA sends more into that now-existing private recipient.
|
|
let owner_id = Ids::owner();
|
|
let owner_account = state.get_account_by_id(owner_id);
|
|
let sender_ata_id = Ids::owner_ata();
|
|
let sender_ata_account = state.get_account_by_id(sender_ata_id);
|
|
|
|
let membership_proof = state
|
|
.get_proof_for_commitment(&Commitment::new(&recipient_id, &recipient_after_shield))
|
|
.expect("recipient's commitment must be in the set");
|
|
let transfer_secret =
|
|
SharedSecretKey::encapsulate_deterministic(&recipient_vpk, &[0u8; 32], 0).0;
|
|
|
|
let owner_pre = AccountWithMetadata::new(owner_account, true, owner_id);
|
|
let sender_ata_pre = AccountWithMetadata::new(sender_ata_account, false, sender_ata_id);
|
|
let recipient_pre =
|
|
AccountWithMetadata::new(recipient_after_shield.clone(), true, recipient_id);
|
|
|
|
let ata_transfer_amount = 200_000_u128;
|
|
let instruction = ata_core::Instruction::Transfer {
|
|
token_program_id: Ids::token_program(),
|
|
amount: ata_transfer_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, sender_ata_pre, recipient_pre],
|
|
Program::serialize_instruction(instruction).unwrap(),
|
|
vec![
|
|
InputAccountIdentity::Public,
|
|
InputAccountIdentity::Public,
|
|
InputAccountIdentity::PrivateAuthorizedUpdate {
|
|
epk: EphemeralPublicKey(Vec::new()),
|
|
view_tag: EncryptedAccountData::compute_view_tag(&recipient_npk, &recipient_vpk),
|
|
ssk: transfer_secret,
|
|
nsk: recipient_nsk,
|
|
membership_proof,
|
|
identifier: 0,
|
|
},
|
|
],
|
|
&program_with_deps,
|
|
)
|
|
.unwrap();
|
|
|
|
let message =
|
|
Message::try_from_circuit_output(vec![owner_id, sender_ata_id], vec![Nonce(0)], output)
|
|
.unwrap();
|
|
let witness_set = WitnessSet::for_message(&message, proof, &[&Keys::owner_key()]);
|
|
state
|
|
.transition_from_privacy_preserving_transaction(
|
|
&PrivacyPreservingTransaction::new(message, witness_set),
|
|
0,
|
|
0,
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
state.get_account_by_id(sender_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 - ata_transfer_amount,
|
|
}),
|
|
nonce: Nonce(0),
|
|
}
|
|
);
|
|
|
|
let recipient_nonce_after = Nonce::private_account_nonce_init(&recipient_id)
|
|
.private_account_nonce_increment(&recipient_nsk);
|
|
let recipient_after_ata_transfer = Account {
|
|
program_owner: Ids::token_program(),
|
|
balance: 0_u128,
|
|
data: Data::from(&TokenHolding::Fungible {
|
|
definition_id: Ids::token_definition(),
|
|
balance: shield_amount + ata_transfer_amount,
|
|
}),
|
|
nonce: recipient_nonce_after,
|
|
};
|
|
assert!(state
|
|
.get_proof_for_commitment(&Commitment::new(
|
|
&recipient_id,
|
|
&recipient_after_ata_transfer
|
|
))
|
|
.is_some());
|
|
}
|
|
|
|
// Marvin-todo
|
|
/// Tests a previously-untried combination: `Burn`'s guest requires `owner` to be a *signer*
|
|
/// (`#[account(signer)]`) — every existing private-owner test so far
|
|
/// (`ata_create_from_private_owner`) only used owner as a passive `PrivateUnauthorized` recipient
|
|
/// in `Create`, which doesn't need signer authorization at all. Here, owner self-initializes *and*
|
|
/// signs in the same transaction via `PrivateAuthorizedInit` (proving control by supplying their
|
|
/// own nsk directly) — the ATA holding itself stays public, per the confirmed `PDA` finding above;
|
|
/// only the signing identity is private.
|
|
#[test]
|
|
fn ata_burn_with_private_owner_signing() {
|
|
let mut state = V03State::new();
|
|
deploy_programs(&mut state);
|
|
state.force_insert_account(Ids::token_definition(), Accounts::token_definition_init());
|
|
|
|
let owner_nsk: NullifierSecretKey = [91u8; 32];
|
|
let owner_npk = NullifierPublicKey::from(&owner_nsk);
|
|
let owner_vpk = ViewingPublicKey::from_seed(&[93u8; 32], &[94u8; 32]);
|
|
let owner_id = AccountId::for_regular_private_account(&owner_npk, 0);
|
|
|
|
// 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 shared_secret = SharedSecretKey::encapsulate_deterministic(&owner_vpk, &[0u8; 32], 0).0;
|
|
|
|
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![
|
|
InputAccountIdentity::PrivateAuthorizedInit {
|
|
epk: EphemeralPublicKey(Vec::new()),
|
|
view_tag: EncryptedAccountData::compute_view_tag(&owner_npk, &owner_vpk),
|
|
ssk: shared_secret,
|
|
nsk: owner_nsk,
|
|
identifier: 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());
|
|
}
|
|
|
|
// Marvin-todo
|
|
/// Composes the `GROUP` dimension with the signer-authorization finding just proven above: a
|
|
/// group-owned owner (GMS distributed through the real seal/unseal handshake, exactly as in
|
|
/// `token_group_owned_holding_shared_control`) signs an `ATA::Burn` via `PrivateAuthorizedInit`.
|
|
/// "Bob" — who only ever receives the sealed GMS, never Alice's `GroupKeyHolder` object —
|
|
/// independently re-derives the identical nsk/npk and successfully signs for the shared ATA
|
|
/// owner identity.
|
|
#[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());
|
|
|
|
// Alice creates the group and derives the shared owner identity's keys.
|
|
let alice_holder = GroupKeyHolder::new();
|
|
let derivation_seed = [13_u8; 32];
|
|
let alice_keys = alice_holder.derive_keys_for_shared_account(&derivation_seed);
|
|
let owner_npk = alice_keys.generate_nullifier_public_key();
|
|
let owner_id = AccountId::for_regular_private_account(&owner_npk, 0);
|
|
|
|
// Alice distributes the GMS to Bob via the real seal/unseal handshake.
|
|
let bob_sealing_keys = SecretSpendingKey([17_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 owner keys and is the one who signs below.
|
|
let bob_keys = bob_holder.derive_keys_for_shared_account(&derivation_seed);
|
|
let bob_nsk = bob_keys.nullifier_secret_key;
|
|
let bob_vpk = bob_keys.generate_viewing_public_key();
|
|
assert_eq!(
|
|
bob_keys.generate_nullifier_public_key(),
|
|
owner_npk,
|
|
"Bob must derive the identical npk as Alice from the shared GMS"
|
|
);
|
|
|
|
// 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 shared_secret = SharedSecretKey::encapsulate_deterministic(&bob_vpk, &[0u8; 32], 0).0;
|
|
|
|
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![
|
|
InputAccountIdentity::PrivateAuthorizedInit {
|
|
epk: EphemeralPublicKey(Vec::new()),
|
|
view_tag: EncryptedAccountData::compute_view_tag(&owner_npk, &bob_vpk),
|
|
ssk: shared_secret,
|
|
nsk: bob_nsk,
|
|
identifier: 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),
|
|
}
|
|
);
|
|
|
|
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());
|
|
}
|