mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-05-12 02:59:29 +00:00
refactor: delegate to auth-transfer, add shared account test
This commit is contained in:
parent
5bf24b191d
commit
f73cd6738f
Binary file not shown.
@ -418,12 +418,50 @@ mod tests {
|
||||
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
/// Group PDA deposit: creates a new PDA and transfers balance from the
|
||||
/// counterparty. Both accounts owned by `private_pda_spender`.
|
||||
/// PDA init: initializes a new PDA under `authenticated_transfer`'s ownership.
|
||||
/// The `private_pda_spender` program chains to `authenticated_transfer` with `pda_seeds`
|
||||
/// to establish authorization and the mask-3 binding.
|
||||
#[test]
|
||||
fn group_pda_deposit() {
|
||||
fn private_pda_init() {
|
||||
let program = Program::private_pda_spender();
|
||||
let noop = Program::noop();
|
||||
let auth_transfer = Program::authenticated_transfer_program();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let shared_secret_pda = SharedSecretKey::new(&[55; 32], &keys.vpk());
|
||||
|
||||
// PDA (new, mask 3) — AccountId derived from private_pda_spender's program ID
|
||||
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk);
|
||||
let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id);
|
||||
|
||||
let auth_id = auth_transfer.id();
|
||||
let program_with_deps =
|
||||
ProgramWithDependencies::new(program, [(auth_id, auth_transfer)].into());
|
||||
|
||||
// is_withdraw=false triggers init path (1 pre-state)
|
||||
let instruction = Program::serialize_instruction((seed, auth_id, 0_u128, false)).unwrap();
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pda_pre],
|
||||
instruction,
|
||||
vec![InputAccountIdentity::PrivatePdaInit {
|
||||
npk,
|
||||
ssk: shared_secret_pda,
|
||||
}],
|
||||
&program_with_deps,
|
||||
);
|
||||
|
||||
let (output, _proof) = result.expect("PDA init should succeed");
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
}
|
||||
|
||||
/// PDA withdraw: chains to `authenticated_transfer` to move balance from PDA to recipient.
|
||||
/// Uses a default PDA (amount=0) because testing with a pre-funded PDA requires a
|
||||
/// two-tx sequence with membership proofs.
|
||||
#[test]
|
||||
fn private_pda_withdraw() {
|
||||
let program = Program::private_pda_spender();
|
||||
let auth_transfer = Program::authenticated_transfer_program();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
@ -433,90 +471,90 @@ mod tests {
|
||||
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk);
|
||||
let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id);
|
||||
|
||||
// Sender (mask 0, public, owned by this program, has balance)
|
||||
// Recipient (mask 0, public)
|
||||
let recipient_id = AccountId::new([88; 32]);
|
||||
let recipient_pre = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: auth_transfer.id(),
|
||||
balance: 10000,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
recipient_id,
|
||||
);
|
||||
|
||||
let auth_id = auth_transfer.id();
|
||||
let program_with_deps =
|
||||
ProgramWithDependencies::new(program, [(auth_id, auth_transfer)].into());
|
||||
|
||||
// is_withdraw=true, amount=0 (PDA has no balance yet)
|
||||
let instruction = Program::serialize_instruction((seed, auth_id, 0_u128, true)).unwrap();
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pda_pre, recipient_pre],
|
||||
instruction,
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
npk,
|
||||
ssk: shared_secret_pda,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program_with_deps,
|
||||
);
|
||||
|
||||
let (output, _proof) = result.expect("PDA withdraw should succeed");
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
}
|
||||
|
||||
/// Shared regular private account: receives funds via `authenticated_transfer` directly,
|
||||
/// no custom program needed. This demonstrates the non-PDA shared account flow where
|
||||
/// keys are derived from GMS via `derive_keys_for_shared_account`. The shared account
|
||||
/// uses standard mask 2 (new unauthorized private) and works with auth-transfer's
|
||||
/// transfer path like any other private account.
|
||||
#[test]
|
||||
fn shared_account_receives_via_auth_transfer() {
|
||||
let program = Program::authenticated_transfer_program();
|
||||
let shared_keys = test_private_account_keys_1();
|
||||
let shared_npk = shared_keys.npk();
|
||||
let shared_identifier: u128 = 42;
|
||||
let shared_secret = SharedSecretKey::new(&[55; 32], &shared_keys.vpk());
|
||||
|
||||
// Sender: public account with balance, owned by auth-transfer
|
||||
let sender_id = AccountId::new([99; 32]);
|
||||
let sender_pre = AccountWithMetadata::new(
|
||||
let sender = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 10000,
|
||||
balance: 1000,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
sender_id,
|
||||
);
|
||||
|
||||
let noop_id = noop.id();
|
||||
let program_with_deps = ProgramWithDependencies::new(program, [(noop_id, noop)].into());
|
||||
// Recipient: shared private account (new, unauthorized, mask 2)
|
||||
let shared_account_id = AccountId::from((&shared_npk, shared_identifier));
|
||||
let recipient = AccountWithMetadata::new(Account::default(), false, shared_account_id);
|
||||
|
||||
let instruction = Program::serialize_instruction((seed, noop_id, 500_u128, true)).unwrap();
|
||||
|
||||
// PDA is mask 3 (private PDA), sender is mask 0 (public).
|
||||
// The noop chained call is required to establish the mask-3 (seed, npk) binding
|
||||
// that the circuit enforces for private PDAs. Without a caller providing pda_seeds,
|
||||
// the circuit's binding check rejects the account.
|
||||
let result = execute_and_prove(
|
||||
vec![pda_pre, sender_pre],
|
||||
instruction,
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
npk,
|
||||
ssk: shared_secret_pda,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program_with_deps,
|
||||
);
|
||||
|
||||
let (output, _proof) = result.expect("group PDA deposit should succeed");
|
||||
// Only PDA (mask 3) produces a commitment; sender (mask 0) is public.
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
}
|
||||
|
||||
/// Group PDA spend binding: the noop chained call with `pda_seeds` establishes
|
||||
/// the mask-3 binding for an existing-but-default PDA. Uses amount=0 because
|
||||
/// testing with a pre-funded PDA requires a two-tx sequence with membership proofs.
|
||||
#[test]
|
||||
fn group_pda_spend_binding() {
|
||||
let program = Program::private_pda_spender();
|
||||
let noop = Program::noop();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let shared_secret_pda = SharedSecretKey::new(&[55; 32], &keys.vpk());
|
||||
|
||||
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk);
|
||||
let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id);
|
||||
|
||||
let bob_id = AccountId::new([88; 32]);
|
||||
let bob_pre = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 10000,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
bob_id,
|
||||
);
|
||||
|
||||
let noop_id = noop.id();
|
||||
let program_with_deps = ProgramWithDependencies::new(program, [(noop_id, noop)].into());
|
||||
|
||||
let instruction = Program::serialize_instruction((seed, noop_id, 0_u128, false)).unwrap();
|
||||
let balance_to_move: u128 = 100;
|
||||
let instruction = Program::serialize_instruction(balance_to_move).unwrap();
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pda_pre, bob_pre],
|
||||
vec![sender, recipient],
|
||||
instruction,
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
npk,
|
||||
ssk: shared_secret_pda,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
npk: shared_npk,
|
||||
ssk: shared_secret,
|
||||
identifier: shared_identifier,
|
||||
},
|
||||
],
|
||||
&program_with_deps,
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
let (output, _proof) = result.expect("group PDA spend binding should succeed");
|
||||
let (output, _proof) = result.expect("shared account receive should succeed");
|
||||
// Sender is public (no commitment), recipient is private (1 commitment)
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,21 +1,25 @@
|
||||
use nssa_core::program::{
|
||||
AccountPostState, ChainedCall, Claim, PdaSeed, ProgramId, ProgramInput, ProgramOutput,
|
||||
AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, ProgramOutput,
|
||||
read_nssa_inputs,
|
||||
};
|
||||
|
||||
/// Single program for group PDA operations. Owns and operates the PDA directly.
|
||||
/// PDA authorization program that delegates balance operations to `authenticated_transfer`.
|
||||
///
|
||||
/// Instruction: `(pda_seed, noop_program_id, amount, is_deposit)`.
|
||||
/// Pre-states: `[group_pda, counterparty]`.
|
||||
/// The PDA is owned by `authenticated_transfer`, not by this program. This program's role
|
||||
/// is solely to provide PDA authorization via `pda_seeds` in chained calls.
|
||||
///
|
||||
/// **Deposit** (`is_deposit = true`, new PDA):
|
||||
/// Claims PDA via `Claim::Pda(seed)`, increases PDA balance, decreases counterparty.
|
||||
/// Counterparty must be authorized and owned by this program (or uninitialized).
|
||||
/// Instruction: `(pda_seed, auth_transfer_id, amount, is_withdraw)`.
|
||||
///
|
||||
/// **Spend** (`is_deposit = false`, existing PDA):
|
||||
/// Decreases PDA balance (this program owns it), increases counterparty.
|
||||
/// Chains to a noop callee with `pda_seeds` to establish the mask-3 binding
|
||||
/// that the circuit requires for existing private PDAs.
|
||||
/// **Init** (`is_withdraw = false`, 1 pre-state `[pda]`):
|
||||
/// Chains to `authenticated_transfer` with `instruction=0` (init path) and `pda_seeds=[seed]`
|
||||
/// to initialize the PDA under `authenticated_transfer`'s ownership.
|
||||
///
|
||||
/// **Withdraw** (`is_withdraw = true`, 2 pre-states `[pda, recipient]`):
|
||||
/// Chains to `authenticated_transfer` with the amount and `pda_seeds=[seed]` to authorize
|
||||
/// the PDA for a balance transfer. The actual balance modification happens in
|
||||
/// `authenticated_transfer`, not here.
|
||||
///
|
||||
/// **Deposit**: done directly via `authenticated_transfer` (no need for this program).
|
||||
type Instruction = (PdaSeed, ProgramId, u128, bool);
|
||||
|
||||
#[expect(
|
||||
@ -32,77 +36,52 @@ fn main() {
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
instruction: (pda_seed, noop_id, amount, is_deposit),
|
||||
instruction: (pda_seed, auth_transfer_id, amount, is_withdraw),
|
||||
},
|
||||
instruction_words,
|
||||
) = read_nssa_inputs::<Instruction>();
|
||||
|
||||
let Ok([pda_pre, counterparty_pre]) = <[_; 2]>::try_from(pre_states.clone()) else {
|
||||
panic!("expected exactly 2 pre_states: [group_pda, counterparty]");
|
||||
};
|
||||
if is_withdraw {
|
||||
let Ok([pda_pre, recipient_pre]) = <[_; 2]>::try_from(pre_states.clone()) else {
|
||||
panic!("expected exactly 2 pre_states for withdraw: [pda, recipient]");
|
||||
};
|
||||
|
||||
if is_deposit {
|
||||
// Deposit: claim PDA, transfer balance from counterparty to PDA.
|
||||
// Both accounts must be owned by this program (or uninitialized) for
|
||||
// validate_execution to allow balance changes.
|
||||
assert!(
|
||||
counterparty_pre.is_authorized,
|
||||
"Counterparty must be authorized to deposit"
|
||||
);
|
||||
// Post-states stay unchanged in this program. The actual balance transfer
|
||||
// happens in the chained call to authenticated_transfer.
|
||||
let pda_post = AccountPostState::new(pda_pre.account.clone());
|
||||
let recipient_post = AccountPostState::new(recipient_pre.account.clone());
|
||||
|
||||
let mut pda_account = pda_pre.account;
|
||||
let mut counterparty_account = counterparty_pre.account;
|
||||
|
||||
pda_account.balance = pda_account
|
||||
.balance
|
||||
.checked_add(amount)
|
||||
.expect("PDA balance overflow");
|
||||
counterparty_account.balance = counterparty_account
|
||||
.balance
|
||||
.checked_sub(amount)
|
||||
.expect("Counterparty has insufficient balance");
|
||||
|
||||
let pda_post = AccountPostState::new_claimed_if_default(pda_account, Claim::Pda(pda_seed));
|
||||
let counterparty_post = AccountPostState::new(counterparty_account);
|
||||
// Chain to authenticated_transfer with pda_seeds to authorize the PDA.
|
||||
// The circuit's resolve_authorization_and_record_bindings establishes the
|
||||
// mask-3 (seed, npk) binding when pda_seeds match the private PDA derivation.
|
||||
let mut auth_pda_pre = pda_pre;
|
||||
auth_pda_pre.is_authorized = true;
|
||||
let auth_call =
|
||||
ChainedCall::new(auth_transfer_id, vec![auth_pda_pre, recipient_pre], &amount)
|
||||
.with_pda_seeds(vec![pda_seed]);
|
||||
|
||||
ProgramOutput::new(
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
instruction_words,
|
||||
pre_states,
|
||||
vec![pda_post, counterparty_post],
|
||||
vec![pda_post, recipient_post],
|
||||
)
|
||||
.with_chained_calls(vec![auth_call])
|
||||
.write();
|
||||
} else {
|
||||
// Spend: decrease PDA balance (owned by this program), increase counterparty.
|
||||
// Chain to noop with pda_seeds to establish the mask-3 binding for the
|
||||
// existing PDA. The noop's pre_states must match our post_states.
|
||||
// Authorization is enforced by the circuit's binding check, not here.
|
||||
// Init: initialize the PDA under authenticated_transfer's ownership.
|
||||
let Ok([pda_pre]) = <[_; 1]>::try_from(pre_states.clone()) else {
|
||||
panic!("expected exactly 1 pre_state for init: [pda]");
|
||||
};
|
||||
|
||||
let mut pda_account = pda_pre.account.clone();
|
||||
let mut counterparty_account = counterparty_pre.account.clone();
|
||||
let pda_post = AccountPostState::new(pda_pre.account.clone());
|
||||
|
||||
pda_account.balance = pda_account
|
||||
.balance
|
||||
.checked_sub(amount)
|
||||
.expect("PDA has insufficient balance");
|
||||
counterparty_account.balance = counterparty_account
|
||||
.balance
|
||||
.checked_add(amount)
|
||||
.expect("Counterparty balance overflow");
|
||||
|
||||
let pda_post = AccountPostState::new(pda_account.clone());
|
||||
let counterparty_post = AccountPostState::new(counterparty_account.clone());
|
||||
|
||||
// Chain to noop solely to establish the mask-3 binding via pda_seeds.
|
||||
let mut noop_pda_pre = pda_pre;
|
||||
noop_pda_pre.account = pda_account;
|
||||
noop_pda_pre.is_authorized = true;
|
||||
|
||||
let mut noop_counterparty_pre = counterparty_pre;
|
||||
noop_counterparty_pre.account = counterparty_account;
|
||||
|
||||
let noop_call = ChainedCall::new(noop_id, vec![noop_pda_pre, noop_counterparty_pre], &())
|
||||
// Chain to authenticated_transfer with instruction=0 (init path) and pda_seeds
|
||||
// to authorize the PDA. authenticated_transfer will claim it with Claim::Authorized.
|
||||
let mut auth_pda_pre = pda_pre;
|
||||
auth_pda_pre.is_authorized = true;
|
||||
let auth_call = ChainedCall::new(auth_transfer_id, vec![auth_pda_pre], &0_u128)
|
||||
.with_pda_seeds(vec![pda_seed]);
|
||||
|
||||
ProgramOutput::new(
|
||||
@ -110,9 +89,9 @@ fn main() {
|
||||
caller_program_id,
|
||||
instruction_words,
|
||||
pre_states,
|
||||
vec![pda_post, counterparty_post],
|
||||
vec![pda_post],
|
||||
)
|
||||
.with_chained_calls(vec![noop_call])
|
||||
.with_chained_calls(vec![auth_call])
|
||||
.write();
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user