mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-08-27 04:11:08 +00:00
fix(lee): check private PDA authorization on first sight
This commit is contained in:
@@ -306,21 +306,16 @@ impl ExecutionState {
|
||||
)
|
||||
});
|
||||
|
||||
let is_authorized = resolve_authorization_and_record_bindings(
|
||||
assert_authorization_and_record_bindings(
|
||||
&mut self.pda_family_binding,
|
||||
&mut self.private_pda_bound_positions,
|
||||
&self.private_pda_by_position,
|
||||
&self.globally_authorized,
|
||||
&caller.authorized_accounts,
|
||||
&caller,
|
||||
caller_pda_seeds,
|
||||
pre_account_id,
|
||||
pre_state_position,
|
||||
caller.program_id,
|
||||
caller_pda_seeds,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
pre_is_authorized, is_authorized,
|
||||
"Inconsistent authorization for account {pre_account_id}",
|
||||
pre_is_authorized,
|
||||
);
|
||||
}
|
||||
Entry::Vacant(_) => {
|
||||
@@ -356,10 +351,6 @@ impl ExecutionState {
|
||||
// Subsequent calls need no re-check because the entry is already recorded on
|
||||
// private_pda_bound_positions.
|
||||
if let Some((authority_program_id, seed)) = external_seed {
|
||||
assert!(
|
||||
!pre.is_authorized,
|
||||
"Private PDA with externally-provided seed must not be authorized at position {pre_state_position}"
|
||||
);
|
||||
bind_private_pda_position(
|
||||
&mut self.private_pda_bound_positions,
|
||||
pre_state_position,
|
||||
@@ -373,13 +364,25 @@ impl ExecutionState {
|
||||
pre_account_id,
|
||||
);
|
||||
}
|
||||
let is_private_pda = self
|
||||
.private_pda_by_position
|
||||
.contains_key(&pre_state_position);
|
||||
if is_private_pda {
|
||||
assert_authorization_and_record_bindings(
|
||||
&mut self.pda_family_binding,
|
||||
&mut self.private_pda_bound_positions,
|
||||
&self.private_pda_by_position,
|
||||
&self.globally_authorized,
|
||||
&caller,
|
||||
caller_pda_seeds,
|
||||
pre_account_id,
|
||||
pre_state_position,
|
||||
pre_is_authorized,
|
||||
);
|
||||
}
|
||||
|
||||
// If an account is regular and authorized, make it globally-authorized.
|
||||
if pre_is_authorized
|
||||
&& !self
|
||||
.private_pda_by_position
|
||||
.contains_key(&pre_state_position)
|
||||
{
|
||||
if pre_is_authorized && !is_private_pda {
|
||||
self.globally_authorized.insert(pre_account_id);
|
||||
}
|
||||
self.pre_states.push(pre);
|
||||
@@ -563,58 +566,60 @@ fn bind_private_pda_position(
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve the authorization state of a `pre_state` seen again in a chained call and record
|
||||
/// any resulting bindings. When a caller seed matches, also records the
|
||||
/// `(caller, seed) → account_id` family binding and, for the private form, marks the position
|
||||
/// in `private_pda_bound_positions`. Only reachable when `caller_program_id.is_some()`,
|
||||
/// top-level flows have no caller-emitted seeds, so binding at top level must come from the
|
||||
/// claim path. Free function so callers can pass individual `&mut self.*` field borrows
|
||||
/// without holding a borrow on the surrounding struct's other fields.
|
||||
/// When a caller seed matches, also records the `(caller, seed) → account_id` family binding
|
||||
/// and, for the private form, marks the position in `private_pda_bound_positions`. Free
|
||||
/// function so callers can pass individual `&mut self.*` field borrows without holding a borrow
|
||||
/// on the surrounding struct's other fields.
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "breaking out a context struct does not buy us anything here"
|
||||
)]
|
||||
fn resolve_authorization_and_record_bindings(
|
||||
fn assert_authorization_and_record_bindings(
|
||||
pda_family_binding: &mut HashMap<(ProgramId, PdaSeed), AccountId>,
|
||||
private_pda_bound_positions: &mut HashMap<usize, (ProgramId, PdaSeed)>,
|
||||
private_pda_by_position: &HashMap<usize, (NullifierPublicKey, ViewingPublicKey, Identifier)>,
|
||||
globally_authorized: &HashSet<AccountId>,
|
||||
caller_authorized: &HashSet<AccountId>,
|
||||
caller: &CallerData,
|
||||
caller_pda_seeds: &[PdaSeed],
|
||||
pre_account_id: AccountId,
|
||||
pre_state_position: usize,
|
||||
caller_program_id: Option<ProgramId>,
|
||||
caller_pda_seeds: &[PdaSeed],
|
||||
) -> bool {
|
||||
pre_is_authorized: bool,
|
||||
) {
|
||||
let matched_caller_seed: Option<(PdaSeed, bool, ProgramId)> =
|
||||
caller_program_id.and_then(|caller| {
|
||||
caller.program_id.and_then(|caller_program_id| {
|
||||
caller_pda_seeds.iter().find_map(|seed| {
|
||||
if AccountId::for_public_pda(&caller, seed) == pre_account_id {
|
||||
return Some((*seed, false, caller));
|
||||
if AccountId::for_public_pda(&caller_program_id, seed) == pre_account_id {
|
||||
return Some((*seed, false, caller_program_id));
|
||||
}
|
||||
if let Some((npk, vpk, identifier)) =
|
||||
private_pda_by_position.get(&pre_state_position)
|
||||
&& AccountId::for_private_pda(&caller, seed, npk, vpk, *identifier)
|
||||
&& AccountId::for_private_pda(&caller_program_id, seed, npk, vpk, *identifier)
|
||||
== pre_account_id
|
||||
{
|
||||
return Some((*seed, true, caller));
|
||||
return Some((*seed, true, caller_program_id));
|
||||
}
|
||||
None
|
||||
})
|
||||
});
|
||||
|
||||
if let Some((seed, is_private_form, caller)) = matched_caller_seed {
|
||||
assert_family_binding(pda_family_binding, caller, seed, pre_account_id);
|
||||
if let Some((seed, is_private_form, caller_program_id)) = matched_caller_seed {
|
||||
assert_family_binding(pda_family_binding, caller_program_id, seed, pre_account_id);
|
||||
if is_private_form {
|
||||
bind_private_pda_position(
|
||||
private_pda_bound_positions,
|
||||
pre_state_position,
|
||||
caller,
|
||||
caller_program_id,
|
||||
seed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
matched_caller_seed.is_some()
|
||||
let is_authorized = matched_caller_seed.is_some()
|
||||
|| globally_authorized.contains(&pre_account_id)
|
||||
|| caller_authorized.contains(&pre_account_id)
|
||||
|| caller.authorized_accounts.contains(&pre_account_id);
|
||||
|
||||
assert_eq!(
|
||||
pre_is_authorized, is_authorized,
|
||||
"Inconsistent authorization for account {pre_account_id}",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,46 +66,28 @@ pub fn compute_circuit_output(
|
||||
WitnessKind::Pda { .. } => pre_state.account_id,
|
||||
};
|
||||
|
||||
match (kind, nullifier) {
|
||||
(
|
||||
WitnessKind::Regular { ask },
|
||||
NullifierWitness::Init { .. } | NullifierWitness::Update { .. },
|
||||
) => {
|
||||
if let Some(ask) = ask {
|
||||
let derived = NullifierSecretKey::from(ask);
|
||||
match nullifier {
|
||||
// Check that the authorization key is actually bound to the
|
||||
// account Id.
|
||||
NullifierWitness::Update { nsk, .. } => assert_eq!(
|
||||
derived, *nsk,
|
||||
"Authorization secret key does not derive this account's nullifier secret key"
|
||||
),
|
||||
NullifierWitness::Init { npk, .. } => assert_eq!(
|
||||
NullifierPublicKey::from(&derived),
|
||||
*npk,
|
||||
"Authorization secret key does not derive this account's nullifier public key"
|
||||
),
|
||||
}
|
||||
if let WitnessKind::Regular { ask } = kind {
|
||||
if let Some(ask) = ask {
|
||||
let derived = NullifierSecretKey::from(ask);
|
||||
match nullifier {
|
||||
// Check that the authorization key is actually bound to the
|
||||
// account Id.
|
||||
NullifierWitness::Update { nsk, .. } => assert_eq!(
|
||||
derived, *nsk,
|
||||
"Authorization secret key does not derive this account's nullifier secret key"
|
||||
),
|
||||
NullifierWitness::Init { npk, .. } => assert_eq!(
|
||||
NullifierPublicKey::from(&derived),
|
||||
*npk,
|
||||
"Authorization secret key does not derive this account's nullifier public key"
|
||||
),
|
||||
}
|
||||
assert_eq!(
|
||||
pre_state.is_authorized,
|
||||
ask.is_some(),
|
||||
"Regular private account authorization must match the supplied credential"
|
||||
);
|
||||
}
|
||||
(WitnessKind::Pda { .. }, NullifierWitness::Init { .. }) => assert!(
|
||||
!pre_state.is_authorized,
|
||||
"Private PDA init requires unauthorized pre_state"
|
||||
),
|
||||
// With an external seed the binding comes from the circuit input and the
|
||||
// pre_state is intentionally unauthorized; without one the binding comes from
|
||||
// a Claim or caller pda_seeds, so the pre_state must already be authorized.
|
||||
// When `binding` is `Some`, execution_state already asserted
|
||||
// `!pre_state.is_authorized`.
|
||||
(WitnessKind::Pda { binding }, NullifierWitness::Update { .. }) => assert!(
|
||||
pre_state.is_authorized ^ binding.is_some(),
|
||||
"Private PDA update requires authorized pre_state or external seed"
|
||||
),
|
||||
assert_eq!(
|
||||
pre_state.is_authorized,
|
||||
ask.is_some(),
|
||||
"Regular private account authorization must match the supplied credential"
|
||||
);
|
||||
}
|
||||
|
||||
let (new_nullifier, new_nonce, view_tag) = match nullifier {
|
||||
|
||||
@@ -54,15 +54,12 @@ pub enum WitnessKind {
|
||||
/// Private PDA. The npk-to-account_id binding is proven upstream via `Claim::Pda(seed)` or a
|
||||
/// caller's `pda_seeds` match. The identifier diversifies the PDA within the
|
||||
/// `(program_id, seed, npk)` family: `AccountId::for_private_pda` uses it as the 4th input.
|
||||
/// An init is unauthorized; on an update, authorization may be established upstream by a
|
||||
/// caller `pda_seeds` match or a previously-seen authorization in a chained call.
|
||||
Pda {
|
||||
/// When `Some((authority_program_id, seed))`, the circuit binds this position via the
|
||||
/// external derivation check
|
||||
/// `AccountId::for_private_pda(authority_program_id, seed, npk, vpk, identifier) ==
|
||||
/// pre_state.account_id` rather than requiring a `Claim::Pda` or caller
|
||||
/// `pda_seeds` to establish the binding. The `pre_state` must have `is_authorized
|
||||
/// == false`.
|
||||
/// `pda_seeds` to establish the binding.
|
||||
binding: Option<(ProgramId, PdaSeed)>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -143,6 +143,14 @@ mod test_methods {
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn undeclaring_pda_delegator() -> Program {
|
||||
Program::new_unchecked(
|
||||
test_methods::UNDECLARING_PDA_DELEGATOR_ID,
|
||||
Cow::Borrowed(test_methods::UNDECLARING_PDA_DELEGATOR_ELF),
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn non_delegating_forwarder() -> Program {
|
||||
Program::new_unchecked(
|
||||
|
||||
@@ -974,7 +974,7 @@ fn private_pda_update_encrypts_pda_kind_with_identifier() {
|
||||
let mut commitment_set = CommitmentSet::with_capacity(1);
|
||||
commitment_set.extend(std::slice::from_ref(&pda_commitment));
|
||||
|
||||
let pda_pre = AccountWithMetadata::new(pda_account, true, pda_id);
|
||||
let pda_pre = AccountWithMetadata::new(pda_account, false, pda_id);
|
||||
let recipient_pre = AccountWithMetadata::new(Account::default(), true, AccountId::new([0; 32]));
|
||||
|
||||
let program_with_deps = ProgramWithDependencies::new(
|
||||
@@ -1013,6 +1013,54 @@ fn private_pda_update_encrypts_pda_kind_with_identifier() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_pda_update_at_root_call_may_not_declare_authorization() {
|
||||
let program = crate::test_methods::pda_spend_proxy();
|
||||
let simple_transfer = crate::test_methods::simple_balance_transfer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let identifier: u128 = 99;
|
||||
let simple_transfer_id = simple_transfer.id();
|
||||
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), identifier);
|
||||
let pda_account = Account {
|
||||
program_owner: simple_transfer_id,
|
||||
balance: 1,
|
||||
..Account::default()
|
||||
};
|
||||
let pda_commitment = Commitment::new(&pda_id, &pda_account);
|
||||
let mut commitment_set = CommitmentSet::with_capacity(1);
|
||||
commitment_set.extend(std::slice::from_ref(&pda_commitment));
|
||||
|
||||
let pda_pre = AccountWithMetadata::new(pda_account, true, pda_id);
|
||||
let recipient_pre = AccountWithMetadata::new(Account::default(), true, AccountId::new([0; 32]));
|
||||
|
||||
let program_with_deps =
|
||||
ProgramWithDependencies::new(program, [(simple_transfer_id, simple_transfer)].into());
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pda_pre, recipient_pre],
|
||||
Program::serialize_instruction((seed, 1_u128, simple_transfer_id)).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::Private(PrivateWitness {
|
||||
vpk: keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
identifier,
|
||||
kind: WitnessKind::Pda { binding: None },
|
||||
nullifier: NullifierWitness::Update {
|
||||
view_tag: 0,
|
||||
nsk: keys.nsk(),
|
||||
membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(),
|
||||
},
|
||||
}),
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program_with_deps,
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_pda_init_identifier_mismatch_fails() {
|
||||
let program = crate::test_methods::pda_claimer();
|
||||
@@ -1020,7 +1068,7 @@ fn private_pda_init_identifier_mismatch_fails() {
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), 5);
|
||||
let pre_state = AccountWithMetadata::new(Account::default(), true, account_id);
|
||||
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pre_state],
|
||||
@@ -1041,6 +1089,36 @@ fn private_pda_init_identifier_mismatch_fails() {
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_pda_init_at_root_call_may_not_declare_authorization() {
|
||||
let program = crate::test_methods::pda_claimer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let identifier: u128 = 5;
|
||||
let account_id =
|
||||
AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), identifier);
|
||||
let pre_state = AccountWithMetadata::new(Account::default(), true, account_id);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pre_state],
|
||||
Program::serialize_instruction(seed).unwrap(),
|
||||
vec![InputAccountIdentity::Private(PrivateWitness {
|
||||
vpk: keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
identifier,
|
||||
kind: WitnessKind::Pda { binding: None },
|
||||
nullifier: NullifierWitness::Init {
|
||||
npk,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
},
|
||||
})],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_pda_update_identifier_mismatch_fails() {
|
||||
let program = crate::test_methods::pda_spend_proxy();
|
||||
@@ -1059,7 +1137,7 @@ fn private_pda_update_identifier_mismatch_fails() {
|
||||
let mut commitment_set = CommitmentSet::with_capacity(1);
|
||||
commitment_set.extend(std::slice::from_ref(&pda_commitment));
|
||||
|
||||
let pda_pre = AccountWithMetadata::new(pda_account, true, pda_id);
|
||||
let pda_pre = AccountWithMetadata::new(pda_account, false, pda_id);
|
||||
let recipient_pre = AccountWithMetadata::new(Account::default(), true, AccountId::new([0; 32]));
|
||||
|
||||
let program_with_deps =
|
||||
|
||||
@@ -815,6 +815,64 @@ fn inherited_scope_passes_through_intermediate_calls() {
|
||||
);
|
||||
}
|
||||
|
||||
fn undeclaring_delegation(delegated: bool, external_binding: bool) -> Result<(), LeeError> {
|
||||
let delegator = crate::test_methods::undeclaring_pda_delegator();
|
||||
let callee = crate::test_methods::auth_asserting_noop();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([77; 32]);
|
||||
|
||||
let delegator_id = delegator.id();
|
||||
let account_id = AccountId::for_private_pda(&delegator_id, &seed, &npk, &keys.vpk(), 0);
|
||||
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
|
||||
|
||||
let callee_id = callee.id();
|
||||
let program_with_deps = ProgramWithDependencies::new(delegator, [(callee_id, callee)].into());
|
||||
|
||||
execute_and_prove(
|
||||
vec![pre_state],
|
||||
Program::serialize_instruction((
|
||||
delegated.then_some(seed),
|
||||
callee_id,
|
||||
Program::serialize_instruction(()).unwrap(),
|
||||
))
|
||||
.unwrap(),
|
||||
vec![InputAccountIdentity::Private(PrivateWitness {
|
||||
vpk: keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
identifier: 0,
|
||||
kind: WitnessKind::Pda {
|
||||
binding: external_binding.then_some((delegator_id, seed)),
|
||||
},
|
||||
nullifier: NullifierWitness::Init {
|
||||
npk,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
},
|
||||
})],
|
||||
&program_with_deps,
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delegated_private_pda_first_seen_in_callee_is_authorized() {
|
||||
undeclaring_delegation(true, true)
|
||||
.expect("a caller's pda_seeds must authorize a private PDA it delegates at first sight");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caller_seeds_bind_a_private_pda_first_seen_in_the_callee() {
|
||||
undeclaring_delegation(true, false)
|
||||
.expect("a caller's pda_seeds must bind a private PDA it delegates at first sight");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undelegated_private_pda_in_a_callee_may_not_declare_authorization() {
|
||||
let result = undeclaring_delegation(false, true);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
/// Exploit-scenario pin. A single `(program_id, seed)` pair can derive a family of
|
||||
/// `AccountId`s, one public PDA and one private PDA per distinct npk. Without the tx-wide
|
||||
/// family-binding check, a program could claim `PDA_alice` (`alice_npk`) and
|
||||
@@ -891,15 +949,13 @@ fn private_pda_top_level_reuse_rejected_by_binding_check() {
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([99; 32]);
|
||||
|
||||
// Simulate a previously-claimed private PDA: program_owner != DEFAULT, is_authorized =
|
||||
// true, account_id derived via the private formula.
|
||||
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), u128::MAX);
|
||||
let owned_pre_state = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
false,
|
||||
account_id,
|
||||
);
|
||||
|
||||
@@ -1354,7 +1410,7 @@ fn two_private_pda_family_members_receive_and_spend() {
|
||||
let recipient_account = state.get_account_by_id(recipient_id);
|
||||
let (output, proof) = execute_and_prove(
|
||||
vec![
|
||||
AccountWithMetadata::new(alice_pda_0_account, true, alice_pda_0_id),
|
||||
AccountWithMetadata::new(alice_pda_0_account, false, alice_pda_0_id),
|
||||
AccountWithMetadata::new(recipient_account, true, recipient_id),
|
||||
],
|
||||
Program::serialize_instruction((seed, amount, simple_transfer_id)).unwrap(),
|
||||
@@ -1393,7 +1449,7 @@ fn two_private_pda_family_members_receive_and_spend() {
|
||||
let recipient_account = state.get_account_by_id(recipient_id);
|
||||
let (output, proof) = execute_and_prove(
|
||||
vec![
|
||||
AccountWithMetadata::new(alice_pda_1_account.clone(), true, alice_pda_1_id),
|
||||
AccountWithMetadata::new(alice_pda_1_account.clone(), false, alice_pda_1_id),
|
||||
AccountWithMetadata::new(recipient_account, false, recipient_id),
|
||||
],
|
||||
Program::serialize_instruction((seed, amount, simple_transfer_id)).unwrap(),
|
||||
|
||||
@@ -5,7 +5,7 @@ use risc0_zkvm::serde::to_vec;
|
||||
|
||||
/// Proxy for spending from a private PDA via `simple_transfer`.
|
||||
///
|
||||
/// `pre_states = [pda (authorized), recipient]`. Debits the PDA and credits the recipient.
|
||||
/// `pre_states = [pda, recipient]`. Debits the PDA and credits the recipient.
|
||||
/// The PDA-to-npk binding is established via `pda_seeds` in the chained call to `simple_transfer`.
|
||||
type Instruction = (PdaSeed, u128, ProgramId);
|
||||
|
||||
@@ -24,15 +24,16 @@ fn main() {
|
||||
return;
|
||||
};
|
||||
|
||||
assert!(first.is_authorized, "first pre_state must be authorized");
|
||||
|
||||
let first_post = AccountPostState::new(first.account.clone());
|
||||
let second_post = AccountPostState::new(second.account.clone());
|
||||
|
||||
let mut first_for_callee = first.clone();
|
||||
first_for_callee.is_authorized = true;
|
||||
|
||||
let chained_call = ChainedCall {
|
||||
program_id: simple_transfer_id,
|
||||
instruction_data: to_vec(&amount).unwrap(),
|
||||
pre_states: vec![first.clone(), second.clone()],
|
||||
pre_states: vec![first_for_callee, second.clone()],
|
||||
pda_seeds: vec![seed],
|
||||
};
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ fn main() {
|
||||
let recipient_post = AccountPostState::new(recipient_pre.account.clone());
|
||||
|
||||
// Chain to simple_transfer with pda_seeds to authorize the PDA.
|
||||
// The circuit's resolve_authorization_and_record_bindings establishes the
|
||||
// The circuit's assert_authorization_and_record_bindings establishes the
|
||||
// private PDA (seed, npk) binding when pda_seeds match the private PDA derivation.
|
||||
let mut auth_pda_pre = pda_pre;
|
||||
auth_pda_pre.is_authorized = true;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
use lee_core::program::{
|
||||
ChainedCall, InstructionData, PdaSeed, ProgramId, ProgramInput, ProgramOutput, read_lee_inputs,
|
||||
};
|
||||
|
||||
type Instruction = (Option<PdaSeed>, ProgramId, InstructionData);
|
||||
|
||||
fn main() {
|
||||
let (
|
||||
ProgramInput {
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
mut pre_states,
|
||||
instruction: (seed, callee_program_id, callee_instruction),
|
||||
},
|
||||
instruction_words,
|
||||
) = read_lee_inputs::<Instruction>();
|
||||
|
||||
let Some(first) = pre_states.first_mut() else {
|
||||
return;
|
||||
};
|
||||
first.is_authorized = true;
|
||||
|
||||
// Emit an output with only chained calls and no pre or post-states.
|
||||
ProgramOutput::new(
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
instruction_words,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
)
|
||||
.with_chained_calls(vec![ChainedCall {
|
||||
program_id: callee_program_id,
|
||||
instruction_data: callee_instruction,
|
||||
pre_states,
|
||||
pda_seeds: seed.into_iter().collect(),
|
||||
}])
|
||||
.write();
|
||||
}
|
||||
@@ -527,20 +527,20 @@ fn private_key_tree_acc_preparation(
|
||||
|
||||
let from_identifier = from_acc.kind.identifier();
|
||||
let from_keys = &from_acc.key_chain;
|
||||
let ask = from_keys.private_key_holder.authorization_secret_key;
|
||||
// A PDA is program-authorized and carries no credential of its own.
|
||||
let ask = (!is_pda).then_some(from_keys.private_key_holder.authorization_secret_key);
|
||||
let nsk = from_keys.private_key_holder.nullifier_secret_key();
|
||||
let from_npk = from_keys.nullifier_public_key;
|
||||
let from_vpk = from_keys.viewing_public_key.clone();
|
||||
|
||||
// TODO: Technically we could allow unauthorized owned accounts, but currently we don't have
|
||||
// support from that in the wallet.
|
||||
let sender_pre = AccountWithMetadata::new(from_acc.account.clone(), true, account_id);
|
||||
let sender_pre = AccountWithMetadata::new(from_acc.account.clone(), ask.is_some(), account_id);
|
||||
|
||||
let random_seed = random_bytes();
|
||||
|
||||
Ok(AccountPreparedData {
|
||||
// A PDA is program-authorized and carries no credential of its own.
|
||||
ask: (!is_pda).then_some(ask),
|
||||
ask,
|
||||
nsk: Some(nsk),
|
||||
npk: from_npk,
|
||||
identifier: from_identifier,
|
||||
@@ -593,7 +593,7 @@ fn private_shared_acc_preparation(
|
||||
.map(|e| e.account.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
let pre_state = AccountWithMetadata::new(acc, true, account_id);
|
||||
let pre_state = AccountWithMetadata::new(acc, ask.is_some(), account_id);
|
||||
|
||||
let random_seed = random_bytes();
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ use risc0_zkvm::serde::to_vec;
|
||||
|
||||
/// Proxy for spending from a private PDA via `auth_transfer`.
|
||||
///
|
||||
/// `pre_states = [pda (authorized), recipient]`. Debits the PDA and credits the recipient.
|
||||
/// `pre_states = [pda, recipient]`. Debits the PDA and credits the recipient.
|
||||
/// The PDA-to-npk binding is established via `pda_seeds` in the chained call to `auth_transfer`.
|
||||
type Instruction = (PdaSeed, u128, ProgramId);
|
||||
|
||||
@@ -24,16 +24,17 @@ fn main() {
|
||||
return;
|
||||
};
|
||||
|
||||
assert!(first.is_authorized, "first pre_state must be authorized");
|
||||
|
||||
let first_post = AccountPostState::new(first.account.clone());
|
||||
let second_post = AccountPostState::new(second.account.clone());
|
||||
|
||||
let mut first_for_callee = first.clone();
|
||||
first_for_callee.is_authorized = true;
|
||||
|
||||
let chained_call = ChainedCall {
|
||||
program_id: auth_transfer_id,
|
||||
instruction_data: to_vec(&authenticated_transfer_core::Instruction::Transfer { amount })
|
||||
.unwrap(),
|
||||
pre_states: vec![first.clone(), second.clone()],
|
||||
pre_states: vec![first_for_callee, second.clone()],
|
||||
pda_seeds: vec![seed],
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user