2026-04-02 12:37:42 +02:00
|
|
|
use std::collections::HashMap;
|
|
|
|
|
|
2026-03-17 18:08:53 +01:00
|
|
|
use ata_core::{compute_ata_seed, get_associated_token_account_id};
|
2026-06-19 12:52:52 -03:00
|
|
|
use integration_tests::TestState as V03State;
|
2026-03-17 18:08:53 +01:00
|
|
|
use nssa::{
|
2026-04-02 12:37:42 +02:00
|
|
|
execute_and_prove,
|
|
|
|
|
privacy_preserving_transaction::{
|
|
|
|
|
circuit::ProgramWithDependencies, Message, PrivacyPreservingTransaction, WitnessSet,
|
|
|
|
|
},
|
|
|
|
|
program::Program,
|
2026-03-17 18:08:53 +01:00
|
|
|
program_deployment_transaction::{self, ProgramDeploymentTransaction},
|
2026-06-19 12:52:52 -03:00
|
|
|
public_transaction, PrivateKey, PublicKey, PublicTransaction, SharedSecretKey,
|
2026-04-02 12:37:42 +02:00
|
|
|
};
|
|
|
|
|
use nssa_core::{
|
|
|
|
|
account::{Account, AccountId, AccountWithMetadata, Data, Nonce},
|
refactor: migrate programs to LEZ lez-core-v0.2.0
Bump the LEZ dependency from the `v0.2.0-rc3` tags to the released
`lez-core-v0.2.0` tag across the workspace and all guest manifests. The crate
was renamed upstream, so `nssa_core`/`nssa` now resolve via the `lee_core`/`lee`
packages, and spel-framework points at the `refactor/lez-v020-compat` fork
branch for compatibility.
Adapt the integration tests to the new API surface:
- `NssaError` is now `LeeError` (error variants unchanged).
- Account inputs move from numeric mask vectors (`vec![2, 0, 0]`) to typed
`InputAccountIdentity` values (e.g. `PrivateUnauthorized { epk, view_tag,
npk, ssk, identifier }`).
- `ViewingPublicKey::from_scalar` → `from_seed(d, z)`; `AccountId::from(&npk)`
→ `AccountId::for_regular_private_account(&npk, 0)`; ephemeral-key/shared-
secret setup → `SharedSecretKey::encapsulate_deterministic(...)` with the
circuit filling the EPK.
Regenerate all guest Cargo.lock files and the workspace lockfile to match.
2026-06-28 14:45:08 +02:00
|
|
|
encryption::{EphemeralPublicKey, ViewingPublicKey},
|
|
|
|
|
EncryptedAccountData, InputAccountIdentity, NullifierPublicKey, NullifierSecretKey,
|
2026-03-17 18:08:53 +01:00
|
|
|
};
|
|
|
|
|
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 {
|
2026-05-13 17:24:11 -03:00
|
|
|
let seed = compute_ata_seed(
|
|
|
|
|
Self::token_program(),
|
|
|
|
|
Self::owner(),
|
|
|
|
|
Self::token_definition(),
|
|
|
|
|
);
|
2026-03-17 18:08:53 +01:00
|
|
|
get_associated_token_account_id(&Self::ata_program(), &seed)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn recipient_ata() -> AccountId {
|
2026-05-13 17:24:11 -03:00
|
|
|
let seed = compute_ata_seed(
|
|
|
|
|
Self::token_program(),
|
|
|
|
|
Self::recipient(),
|
|
|
|
|
Self::token_definition(),
|
|
|
|
|
);
|
2026-03-17 18:08:53 +01:00
|
|
|
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,
|
feat(token): add mint authority model to token program
Add an optional mint authority to fungible tokens for controlled supply:
create with a designated minter, mint additional supply, rotate the
authority to a new key, or permanently revoke it to fix the supply.
The authority is stored inline on `TokenDefinition::Fungible` as
`authority: Option<AccountId>` (`Some(id)` = mintable by `id`, `None` =
fixed supply). Keeping it a plain `Option<AccountId>` rather than a custom
wrapper type leaves account state decodable by `spel inspect`; the
require/rotate/revoke guard logic lives inline in the handlers.
LEZ rejects a transaction that lists the same account id twice, so one
instruction cannot statically express both "the definition account is the
authority and signs" (self/PDA authority) and "a distinct rotated account
signs" (external authority) — they need opposite signer markers. Each
privileged operation is therefore split into a self and an external
variant:
- `Mint` / `SetAuthority` — the definition account is the signer.
- `MintWithAuthority` / `SetAuthorityWithAuthority` — a distinct authority
account is the signer; the definition account does not sign.
Creation via `NewFungibleDefinition { mint_authority, .. }`; an all-zero
authority id is rejected. The AMM's LP token uses self/PDA authority — its
stored authority is the LP definition PDA, minted only by the pool via
chained calls.
Covered by token unit tests and zkVM integration tests: creation with and
without an authority, self- and external-authority mint, rotation, and
external rotate/revoke. IDLs regenerated.
2026-05-27 15:04:28 +05:30
|
|
|
authority: None,
|
2026-03-17 18:08:53 +01:00
|
|
|
}),
|
|
|
|
|
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),
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-15 14:55:04 -03:00
|
|
|
|
|
|
|
|
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),
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-13 17:24:11 -03:00
|
|
|
|
|
|
|
|
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,
|
feat(token): add mint authority model to token program
Add an optional mint authority to fungible tokens for controlled supply:
create with a designated minter, mint additional supply, rotate the
authority to a new key, or permanently revoke it to fix the supply.
The authority is stored inline on `TokenDefinition::Fungible` as
`authority: Option<AccountId>` (`Some(id)` = mintable by `id`, `None` =
fixed supply). Keeping it a plain `Option<AccountId>` rather than a custom
wrapper type leaves account state decodable by `spel inspect`; the
require/rotate/revoke guard logic lives inline in the handlers.
LEZ rejects a transaction that lists the same account id twice, so one
instruction cannot statically express both "the definition account is the
authority and signs" (self/PDA authority) and "a distinct rotated account
signs" (external authority) — they need opposite signer markers. Each
privileged operation is therefore split into a self and an external
variant:
- `Mint` / `SetAuthority` — the definition account is the signer.
- `MintWithAuthority` / `SetAuthorityWithAuthority` — a distinct authority
account is the signer; the definition account does not sign.
Creation via `NewFungibleDefinition { mint_authority, .. }`; an all-zero
authority id is rejected. The AMM's LP token uses self/PDA authority — its
stored authority is the LP definition PDA, minted only by the pool via
chained calls.
Covered by token unit tests and zkVM integration tests: creation with and
without an authority, self- and external-authority mint, rotation, and
external rotate/revoke. IDLs regenerated.
2026-05-27 15:04:28 +05:30
|
|
|
authority: None,
|
2026-05-13 17:24:11 -03:00
|
|
|
}),
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-17 18:08:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 {
|
2026-06-29 01:16:34 +02:00
|
|
|
let mut state = V03State::new();
|
2026-03-17 18:08:53 +01:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 14:55:04 -03:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 18:08:53 +01:00
|
|
|
#[test]
|
|
|
|
|
fn ata_create() {
|
2026-06-29 01:16:34 +02:00
|
|
|
let mut state = V03State::new();
|
2026-03-17 18:08:53 +01:00
|
|
|
deploy_programs(&mut state);
|
|
|
|
|
state.force_insert_account(Ids::token_definition(), Accounts::token_definition_init());
|
|
|
|
|
|
2026-05-13 17:24:11 -03:00
|
|
|
let instruction = ata_core::Instruction::Create {
|
|
|
|
|
token_program_id: Ids::token_program(),
|
|
|
|
|
};
|
2026-03-17 18:08:53 +01:00
|
|
|
|
|
|
|
|
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);
|
2026-04-15 14:55:04 -03:00
|
|
|
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
|
2026-03-17 18:08:53 +01:00
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
2026-05-13 17:24:11 -03:00
|
|
|
let instruction = ata_core::Instruction::Create {
|
|
|
|
|
token_program_id: Ids::token_program(),
|
|
|
|
|
};
|
2026-03-17 18:08:53 +01:00
|
|
|
|
|
|
|
|
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);
|
2026-04-15 14:55:04 -03:00
|
|
|
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
|
2026-03-17 18:08:53 +01:00
|
|
|
|
|
|
|
|
// 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),
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 17:24:11 -03:00
|
|
|
#[test]
|
|
|
|
|
fn ata_create_rejects_definition_owned_by_unexpected_token_program() {
|
2026-06-29 01:16:34 +02:00
|
|
|
let mut state = V03State::new();
|
2026-05-13 17:24:11 -03:00
|
|
|
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() {
|
2026-06-29 01:16:34 +02:00
|
|
|
let mut state = V03State::new();
|
2026-05-13 17:24:11 -03:00
|
|
|
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() {
|
2026-06-29 01:16:34 +02:00
|
|
|
let mut state = V03State::new();
|
2026-05-13 17:24:11 -03:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 18:08:53 +01:00
|
|
|
#[test]
|
|
|
|
|
fn ata_transfer() {
|
2026-04-15 14:55:04 -03:00
|
|
|
let mut state = state_for_ata_tests_with_precreated_recipient_ata();
|
2026-03-17 18:08:53 +01:00
|
|
|
|
|
|
|
|
let instruction = ata_core::Instruction::Transfer {
|
2026-05-13 17:24:11 -03:00
|
|
|
token_program_id: Ids::token_program(),
|
2026-03-17 18:08:53 +01:00
|
|
|
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);
|
2026-04-15 14:55:04 -03:00
|
|
|
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
|
2026-03-17 18:08:53 +01:00
|
|
|
|
|
|
|
|
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),
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
fix(ata): lock down `ATA::Transfer` recipient contract
Enforce at the ATA layer that the recipient token holding is already
initialized, owned by the same token program as the sender ATA, decodes
to a valid `TokenHolding`, and points at the same token definition as
the sender. Align the core instruction doc and guest wrapper doc with
that contract, and cover the boundary with unit tests (default,
foreign-owned, malformed, mismatched-definition recipients, plus the
missing-owner-auth and happy paths) and end-to-end integration tests
(default and mismatched-definition recipients).
Without this, the downstream `token::Transfer` default-recipient
`Claim::Authorized` path was reachable through ATA, so integrators had
to reverse-engineer recipient semantics from token/runtime internals.
2026-05-11 12:44:46 -03:00
|
|
|
#[test]
|
|
|
|
|
fn ata_transfer_rejects_default_recipient() {
|
|
|
|
|
let mut state = state_for_ata_tests();
|
|
|
|
|
|
2026-05-13 17:24:11 -03:00
|
|
|
let instruction = ata_core::Instruction::Transfer {
|
|
|
|
|
token_program_id: Ids::token_program(),
|
|
|
|
|
amount: 1_u128,
|
|
|
|
|
};
|
fix(ata): lock down `ATA::Transfer` recipient contract
Enforce at the ATA layer that the recipient token holding is already
initialized, owned by the same token program as the sender ATA, decodes
to a valid `TokenHolding`, and points at the same token definition as
the sender. Align the core instruction doc and guest wrapper doc with
that contract, and cover the boundary with unit tests (default,
foreign-owned, malformed, mismatched-definition recipients, plus the
missing-owner-auth and happy paths) and end-to-end integration tests
(default and mismatched-definition recipients).
Without this, the downstream `token::Transfer` default-recipient
`Claim::Authorized` path was reachable through ATA, so integrators had
to reverse-engineer recipient semantics from token/runtime internals.
2026-05-11 12:44:46 -03:00
|
|
|
|
|
|
|
|
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());
|
|
|
|
|
|
2026-05-13 17:24:11 -03:00
|
|
|
let instruction = ata_core::Instruction::Transfer {
|
|
|
|
|
token_program_id: Ids::token_program(),
|
|
|
|
|
amount: 1_u128,
|
|
|
|
|
};
|
fix(ata): lock down `ATA::Transfer` recipient contract
Enforce at the ATA layer that the recipient token holding is already
initialized, owned by the same token program as the sender ATA, decodes
to a valid `TokenHolding`, and points at the same token definition as
the sender. Align the core instruction doc and guest wrapper doc with
that contract, and cover the boundary with unit tests (default,
foreign-owned, malformed, mismatched-definition recipients, plus the
missing-owner-auth and happy paths) and end-to-end integration tests
(default and mismatched-definition recipients).
Without this, the downstream `token::Transfer` default-recipient
`Claim::Authorized` path was reachable through ATA, so integrators had
to reverse-engineer recipient semantics from token/runtime internals.
2026-05-11 12:44:46 -03:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 18:08:53 +01:00
|
|
|
#[test]
|
|
|
|
|
fn ata_burn() {
|
|
|
|
|
let mut state = state_for_ata_tests();
|
|
|
|
|
|
|
|
|
|
let instruction = ata_core::Instruction::Burn {
|
2026-05-13 17:24:11 -03:00
|
|
|
token_program_id: Ids::token_program(),
|
2026-03-17 18:08:53 +01:00
|
|
|
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);
|
2026-04-15 14:55:04 -03:00
|
|
|
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
|
2026-03-17 18:08:53 +01:00
|
|
|
|
|
|
|
|
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,
|
feat(token): add mint authority model to token program
Add an optional mint authority to fungible tokens for controlled supply:
create with a designated minter, mint additional supply, rotate the
authority to a new key, or permanently revoke it to fix the supply.
The authority is stored inline on `TokenDefinition::Fungible` as
`authority: Option<AccountId>` (`Some(id)` = mintable by `id`, `None` =
fixed supply). Keeping it a plain `Option<AccountId>` rather than a custom
wrapper type leaves account state decodable by `spel inspect`; the
require/rotate/revoke guard logic lives inline in the handlers.
LEZ rejects a transaction that lists the same account id twice, so one
instruction cannot statically express both "the definition account is the
authority and signs" (self/PDA authority) and "a distinct rotated account
signs" (external authority) — they need opposite signer markers. Each
privileged operation is therefore split into a self and an external
variant:
- `Mint` / `SetAuthority` — the definition account is the signer.
- `MintWithAuthority` / `SetAuthorityWithAuthority` — a distinct authority
account is the signer; the definition account does not sign.
Creation via `NewFungibleDefinition { mint_authority, .. }`; an all-zero
authority id is rejected. The AMM's LP token uses self/PDA authority — its
stored authority is the LP definition PDA, minted only by the pool via
chained calls.
Covered by token unit tests and zkVM integration tests: creation with and
without an authority, self- and external-authority mint, rotation, and
external rotate/revoke. IDLs regenerated.
2026-05-27 15:04:28 +05:30
|
|
|
authority: None,
|
2026-03-17 18:08:53 +01:00
|
|
|
}),
|
|
|
|
|
nonce: Nonce(0),
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-04-02 12:37:42 +02:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn ata_create_from_private_owner() {
|
2026-06-29 01:16:34 +02:00
|
|
|
let mut state = V03State::new();
|
2026-04-02 12:37:42 +02:00
|
|
|
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);
|
refactor: migrate programs to LEZ lez-core-v0.2.0
Bump the LEZ dependency from the `v0.2.0-rc3` tags to the released
`lez-core-v0.2.0` tag across the workspace and all guest manifests. The crate
was renamed upstream, so `nssa_core`/`nssa` now resolve via the `lee_core`/`lee`
packages, and spel-framework points at the `refactor/lez-v020-compat` fork
branch for compatibility.
Adapt the integration tests to the new API surface:
- `NssaError` is now `LeeError` (error variants unchanged).
- Account inputs move from numeric mask vectors (`vec![2, 0, 0]`) to typed
`InputAccountIdentity` values (e.g. `PrivateUnauthorized { epk, view_tag,
npk, ssk, identifier }`).
- `ViewingPublicKey::from_scalar` → `from_seed(d, z)`; `AccountId::from(&npk)`
→ `AccountId::for_regular_private_account(&npk, 0)`; ephemeral-key/shared-
secret setup → `SharedSecretKey::encapsulate_deterministic(...)` with the
circuit filling the EPK.
Regenerate all guest Cargo.lock files and the workspace lockfile to match.
2026-06-28 14:45:08 +02:00
|
|
|
// `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);
|
2026-04-02 12:37:42 +02:00
|
|
|
|
|
|
|
|
// ATA derived from the private owner
|
2026-05-13 17:24:11 -03:00
|
|
|
let seed = compute_ata_seed(Ids::token_program(), owner_id, Ids::token_definition());
|
2026-04-02 12:37:42 +02:00
|
|
|
let owner_ata_id = get_associated_token_account_id(&Ids::ata_program(), &seed);
|
|
|
|
|
|
refactor: migrate programs to LEZ lez-core-v0.2.0
Bump the LEZ dependency from the `v0.2.0-rc3` tags to the released
`lez-core-v0.2.0` tag across the workspace and all guest manifests. The crate
was renamed upstream, so `nssa_core`/`nssa` now resolve via the `lee_core`/`lee`
packages, and spel-framework points at the `refactor/lez-v020-compat` fork
branch for compatibility.
Adapt the integration tests to the new API surface:
- `NssaError` is now `LeeError` (error variants unchanged).
- Account inputs move from numeric mask vectors (`vec![2, 0, 0]`) to typed
`InputAccountIdentity` values (e.g. `PrivateUnauthorized { epk, view_tag,
npk, ssk, identifier }`).
- `ViewingPublicKey::from_scalar` → `from_seed(d, z)`; `AccountId::from(&npk)`
→ `AccountId::for_regular_private_account(&npk, 0)`; ephemeral-key/shared-
secret setup → `SharedSecretKey::encapsulate_deterministic(...)` with the
circuit filling the EPK.
Regenerate all guest Cargo.lock files and the workspace lockfile to match.
2026-06-28 14:45:08 +02:00
|
|
|
// Pre-states: private uninitialized owner, public token definition, public uninitialized ATA.
|
2026-04-02 12:37:42 +02:00
|
|
|
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);
|
|
|
|
|
|
2026-05-13 17:24:11 -03:00
|
|
|
let instruction = ata_core::Instruction::Create {
|
|
|
|
|
token_program_id: Ids::token_program(),
|
|
|
|
|
};
|
2026-04-02 12:37:42 +02:00
|
|
|
let instruction_data = Program::serialize_instruction(instruction).unwrap();
|
|
|
|
|
|
refactor: migrate programs to LEZ lez-core-v0.2.0
Bump the LEZ dependency from the `v0.2.0-rc3` tags to the released
`lez-core-v0.2.0` tag across the workspace and all guest manifests. The crate
was renamed upstream, so `nssa_core`/`nssa` now resolve via the `lee_core`/`lee`
packages, and spel-framework points at the `refactor/lez-v020-compat` fork
branch for compatibility.
Adapt the integration tests to the new API surface:
- `NssaError` is now `LeeError` (error variants unchanged).
- Account inputs move from numeric mask vectors (`vec![2, 0, 0]`) to typed
`InputAccountIdentity` values (e.g. `PrivateUnauthorized { epk, view_tag,
npk, ssk, identifier }`).
- `ViewingPublicKey::from_scalar` → `from_seed(d, z)`; `AccountId::from(&npk)`
→ `AccountId::for_regular_private_account(&npk, 0)`; ephemeral-key/shared-
secret setup → `SharedSecretKey::encapsulate_deterministic(...)` with the
circuit filling the EPK.
Regenerate all guest Cargo.lock files and the workspace lockfile to match.
2026-06-28 14:45:08 +02:00
|
|
|
// 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;
|
2026-04-02 12:37:42 +02:00
|
|
|
|
2026-06-29 01:16:34 +02:00
|
|
|
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();
|
2026-04-02 12:37:42 +02:00
|
|
|
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,
|
refactor: migrate programs to LEZ lez-core-v0.2.0
Bump the LEZ dependency from the `v0.2.0-rc3` tags to the released
`lez-core-v0.2.0` tag across the workspace and all guest manifests. The crate
was renamed upstream, so `nssa_core`/`nssa` now resolve via the `lee_core`/`lee`
packages, and spel-framework points at the `refactor/lez-v020-compat` fork
branch for compatibility.
Adapt the integration tests to the new API surface:
- `NssaError` is now `LeeError` (error variants unchanged).
- Account inputs move from numeric mask vectors (`vec![2, 0, 0]`) to typed
`InputAccountIdentity` values (e.g. `PrivateUnauthorized { epk, view_tag,
npk, ssk, identifier }`).
- `ViewingPublicKey::from_scalar` → `from_seed(d, z)`; `AccountId::from(&npk)`
→ `AccountId::for_regular_private_account(&npk, 0)`; ephemeral-key/shared-
secret setup → `SharedSecretKey::encapsulate_deterministic(...)` with the
circuit filling the EPK.
Regenerate all guest Cargo.lock files and the workspace lockfile to match.
2026-06-28 14:45:08 +02:00
|
|
|
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,
|
|
|
|
|
],
|
2026-04-02 12:37:42 +02:00
|
|
|
&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
|
2026-04-15 14:55:04 -03:00
|
|
|
.transition_from_privacy_preserving_transaction(&tx, 0, 0)
|
2026-04-02 12:37:42 +02:00
|
|
|
.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),
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|