mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-07-03 05:29:50 +00:00
Addresses @0x-r4bbit's review:
- lez-authority now provides an Authority(Option<[u8;32]>) newtype and an
Ownable trait (require_owner / transfer_ownership / renounce_ownership);
programs embed the authority slot in their account type instead of calling
a wrapper. Replaces the old AuthoritySlot.
- TokenDefinition::Fungible embeds authority: Authority; TokenDefinition
implements Ownable.
- Fold mint authority into NewFungibleDefinition { mint_authority: Option<AccountId> };
remove the separate NewFungibleDefinitionWithAuthority instruction.
- mint/set_authority authorize against the definition account itself (its id
must match the stored authority and be authorized in the tx), restoring the
2-account mint shape and supporting PDA authorities.
- Fix AMM: the pool-definition PDA is now the LP token's mint authority, so the
AMM mints LP at creation and on add-liquidity (was permanently revoked).
- Instruction params use AccountId; remove LP-0013-specific comments.
- Regenerate token/amm/ata/stablecoin IDLs.
Tests: lez-authority 8, token unit 56, token/amm/stablecoin/ata integration all
green under RISC0_DEV_MODE=1; fmt + clippy clean.
1072 lines
34 KiB
Rust
1072 lines
34 KiB
Rust
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, 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: token_core::Authority::new(
|
|
Ids::token_definition()
|
|
.as_ref()
|
|
.try_into()
|
|
.expect("AccountId is always 32 bytes"),
|
|
),
|
|
}),
|
|
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: token_core::Authority::new(
|
|
Ids::token_definition()
|
|
.as_ref()
|
|
.try_into()
|
|
.expect("AccountId is always 32 bytes"),
|
|
),
|
|
}),
|
|
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: token_core::Authority::renounced(),
|
|
}),
|
|
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: token_core::Authority::new(
|
|
Ids::token_definition()
|
|
.as_ref()
|
|
.try_into()
|
|
.expect("AccountId is always 32 bytes")
|
|
),
|
|
}),
|
|
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: token_core::Authority::new(
|
|
Ids::token_definition()
|
|
.as_ref()
|
|
.try_into()
|
|
.expect("AccountId is always 32 bytes")
|
|
),
|
|
}),
|
|
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: token_core::Authority::new(
|
|
Ids::token_definition()
|
|
.as_ref()
|
|
.try_into()
|
|
.expect("AccountId is always 32 bytes")
|
|
),
|
|
}),
|
|
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());
|
|
}
|
|
|
|
#[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());
|
|
}
|
|
|
|
#[test]
|
|
fn token_new_fungible_definition_with_authority() {
|
|
let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0);
|
|
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: token_core::Authority::new(authority_key),
|
|
}),
|
|
nonce: Nonce(1),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn token_set_authority_revoke() {
|
|
let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0);
|
|
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: token_core::Authority::renounced(),
|
|
}),
|
|
nonce: Nonce(2),
|
|
}
|
|
);
|
|
}
|