2026-05-12 08:46:36 +02:00

779 lines
23 KiB
Rust

use nssa::{
execute_and_prove,
privacy_preserving_transaction::{Message, WitnessSet},
program::Program,
program_deployment_transaction::{self, ProgramDeploymentTransaction},
public_transaction, PrivacyPreservingTransaction, PrivateKey, PublicKey, PublicTransaction,
SharedSecretKey, V03State,
};
use nssa_core::{
account::{Account, AccountId, AccountWithMetadata, Data, Nonce},
encryption::{EphemeralPublicKey, ViewingPublicKey},
Commitment, 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")
}
}
impl Ids {
fn token_program() -> nssa_core::program::ProgramId {
token_methods::TOKEN_ID
}
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()))
}
}
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,
}),
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 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_with_genesis_accounts(&[], vec![], 0);
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
}
fn state_for_token_tests_without_recipient() -> V03State {
let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0);
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
}
#[test]
fn token_new_fungible_definition() {
let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0);
deploy_token(&mut state);
let instruction = token_core::Instruction::NewFungibleDefinition {
name: String::from("Gold"),
total_supply: 1_000_000_u128,
};
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,
}),
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_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,
}),
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,
}),
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_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,
}),
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())
}
fn holder_vsk() -> [u8; 32] {
[73; 32]
}
fn holder_vpk() -> ViewingPublicKey {
ViewingPublicKey::from_scalar(Self::holder_vsk())
}
fn recipient_nsk() -> NullifierSecretKey {
[84; 32]
}
fn recipient_npk() -> NullifierPublicKey {
NullifierPublicKey::from(&Self::recipient_nsk())
}
fn recipient_vsk() -> [u8; 32] {
[48; 32]
}
fn recipient_vpk() -> ViewingPublicKey {
ViewingPublicKey::from_scalar(Self::recipient_vsk())
}
}
fn token_program() -> Program {
Program::new(token_methods::TOKEN_ELF.to_vec()).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.
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 sender = AccountWithMetadata::new(sender_account, true, sender_id);
let recipient =
AccountWithMetadata::new(Account::default(), false, &PrivateKeys::recipient_npk());
let esk = [99u8; 32];
let shared_secret = SharedSecretKey::new(&esk, &PrivateKeys::recipient_vpk());
let epk = EphemeralPublicKey::from_scalar(esk);
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![0, 2],
vec![(PrivateKeys::recipient_npk(), shared_secret)],
vec![],
vec![None],
&token_program().into(),
)
.unwrap();
let message = Message::try_from_circuit_output(
vec![sender_id],
vec![sender_nonce],
vec![(
PrivateKeys::recipient_npk(),
PrivateKeys::recipient_vpk(),
epk,
)],
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(&PrivateKeys::recipient_npk()),
}
}
#[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_npk(), &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 new_recipient_npk = PrivateKeys::holder_npk();
let new_recipient_vpk = PrivateKeys::holder_vpk();
let sender_commitment = Commitment::new(&sender_npk, &sender_account);
let esk_1 = [11u8; 32];
let shared_secret_1 = SharedSecretKey::new(&esk_1, &sender_vpk);
let epk_1 = EphemeralPublicKey::from_scalar(esk_1);
let esk_2 = [22u8; 32];
let shared_secret_2 = SharedSecretKey::new(&esk_2, &new_recipient_vpk);
let epk_2 = EphemeralPublicKey::from_scalar(esk_2);
let sender_pre = AccountWithMetadata::new(sender_account.clone(), true, &sender_npk);
let new_recipient_pre = AccountWithMetadata::new(Account::default(), false, &new_recipient_npk);
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![1, 2],
vec![
(sender_npk, shared_secret_1),
(new_recipient_npk, shared_secret_2),
],
vec![sender_nsk],
vec![state.get_proof_for_commitment(&sender_commitment), None],
&token_program().into(),
)
.unwrap();
let message = Message::try_from_circuit_output(
vec![],
vec![],
vec![
(sender_npk, sender_vpk, epk_1),
(new_recipient_npk, new_recipient_vpk, epk_2),
],
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_npk).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_npk, &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_npk),
};
assert!(state
.get_proof_for_commitment(&Commitment::new(&new_recipient_npk, &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 public_recipient_id = Ids::recipient();
let sender_commitment = Commitment::new(&sender_npk, &sender_account);
let esk = [55u8; 32];
let shared_secret = SharedSecretKey::new(&esk, &sender_vpk);
let epk = EphemeralPublicKey::from_scalar(esk);
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_npk);
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![1, 0],
vec![(sender_npk, shared_secret)],
vec![sender_nsk],
vec![state.get_proof_for_commitment(&sender_commitment)],
&token_program().into(),
)
.unwrap();
let message = Message::try_from_circuit_output(
vec![public_recipient_id],
vec![],
vec![(sender_npk, sender_vpk, epk)],
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_npk).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_npk, &new_sender_account))
.is_some());
}