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};
|
|
|
|
|
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},
|
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
|
|
|
public_transaction, PrivateKey, PublicKey, PublicTransaction, SharedSecretKey, V03State,
|
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,
|
2026-06-06 03:15:30 +05:30
|
|
|
authority: token_core::Authority::renounced(),
|
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,
|
2026-06-06 03:15:30 +05:30
|
|
|
authority: token_core::Authority::renounced(),
|
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,
|
2026-06-06 03:15:30 +05:30
|
|
|
authority: token_core::Authority::renounced(),
|
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),
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|