mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-05-08 09:09:31 +00:00
Merge branch 'main' into Pravdyvy/indexer-ffi-spawns-rpc-for-communication
This commit is contained in:
commit
02949e961a
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -7154,9 +7154,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.12"
|
||||
version = "0.103.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06"
|
||||
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
|
||||
@ -154,6 +154,14 @@ opt-level = 'z'
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
|
||||
# Keep backtraces but drop full DWARF type info to avoid LLD OOM/SIGBUS when
|
||||
# linking large integration-test binaries on resource-constrained CI runners.
|
||||
[profile.dev]
|
||||
debug = "line-tables-only"
|
||||
|
||||
[profile.test]
|
||||
debug = "line-tables-only"
|
||||
|
||||
[workspace.lints.rust]
|
||||
warnings = "deny"
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
artifacts/test_program_methods/auth_asserting_noop.bin
Normal file
BIN
artifacts/test_program_methods/auth_asserting_noop.bin
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
artifacts/test_program_methods/pda_claimer.bin
Normal file
BIN
artifacts/test_program_methods/pda_claimer.bin
Normal file
Binary file not shown.
Binary file not shown.
BIN
artifacts/test_program_methods/private_pda_delegator.bin
Normal file
BIN
artifacts/test_program_methods/private_pda_delegator.bin
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
artifacts/test_program_methods/two_pda_claimer.bin
Normal file
BIN
artifacts/test_program_methods/two_pda_claimer.bin
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -52,7 +52,7 @@ The derivation works as follows:
|
||||
|
||||
```
|
||||
seed = SHA256(owner_id || definition_id)
|
||||
ata_address = AccountId::from((ata_program_id, seed))
|
||||
ata_address = AccountId::for_public_pda(ata_program_id, seed)
|
||||
```
|
||||
|
||||
Because the computation is pure, anyone who knows the owner and definition can reproduce the exact same ATA address — no network call required.
|
||||
|
||||
@ -46,7 +46,7 @@ async fn main() {
|
||||
let program = Program::new(bytecode).unwrap();
|
||||
|
||||
// Compute the PDA to pass it as input account to the public execution
|
||||
let pda = AccountId::from((&program.id(), &PDA_SEED));
|
||||
let pda = AccountId::for_public_pda(&program.id(), &PDA_SEED);
|
||||
let account_ids = vec![pda];
|
||||
let instruction_data = ();
|
||||
let nonces = vec![];
|
||||
|
||||
@ -121,7 +121,7 @@ impl InitialData {
|
||||
self.private_accounts
|
||||
.iter()
|
||||
.map(|(key_chain, account)| PrivateAccountPublicInitialData {
|
||||
npk: key_chain.nullifier_public_key.clone(),
|
||||
npk: key_chain.nullifier_public_key,
|
||||
account: account.clone(),
|
||||
})
|
||||
.collect()
|
||||
|
||||
@ -249,10 +249,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
|
||||
vec![sender_pre, recipient_pre],
|
||||
Program::serialize_instruction(balance_to_move).unwrap(),
|
||||
vec![1, 2],
|
||||
vec![
|
||||
(sender_npk.clone(), sender_ss),
|
||||
(recipient_npk.clone(), recipient_ss),
|
||||
],
|
||||
vec![(sender_npk, sender_ss), (recipient_npk, recipient_ss)],
|
||||
vec![sender_nsk],
|
||||
vec![Some(proof)],
|
||||
&program.into(),
|
||||
|
||||
@ -17,6 +17,7 @@ pub struct PrivacyPreservingCircuitInput {
|
||||
/// - `0` - public account
|
||||
/// - `1` - private account with authentication
|
||||
/// - `2` - private account without authentication
|
||||
/// - `3` - private PDA account
|
||||
pub visibility_mask: Vec<u8>,
|
||||
/// Public keys of private accounts.
|
||||
pub private_account_keys: Vec<(NullifierPublicKey, SharedSecretKey)>,
|
||||
|
||||
@ -4,8 +4,8 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{Commitment, account::AccountId};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(any(feature = "host", test), derive(Clone, Hash))]
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(any(feature = "host", test), derive(Hash))]
|
||||
pub struct NullifierPublicKey(pub [u8; 32]);
|
||||
|
||||
impl From<&NullifierPublicKey> for AccountId {
|
||||
|
||||
@ -6,7 +6,7 @@ use risc0_zkvm::{DeserializeOwned, guest::env, serde::Deserializer};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
BlockId, Timestamp,
|
||||
BlockId, NullifierPublicKey, Timestamp,
|
||||
account::{Account, AccountId, AccountWithMetadata},
|
||||
};
|
||||
|
||||
@ -27,7 +27,7 @@ pub struct ProgramInput<T> {
|
||||
/// Each program can derive up to `2^256` unique account IDs by choosing different
|
||||
/// seeds. PDAs allow programs to control namespaced account identifiers without
|
||||
/// collisions between programs.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
|
||||
pub struct PdaSeed([u8; 32]);
|
||||
|
||||
impl PdaSeed {
|
||||
@ -37,8 +37,10 @@ impl PdaSeed {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&ProgramId, &PdaSeed)> for AccountId {
|
||||
fn from(value: (&ProgramId, &PdaSeed)) -> Self {
|
||||
impl AccountId {
|
||||
/// Derives an [`AccountId`] for a public PDA from the program ID and seed.
|
||||
#[must_use]
|
||||
pub fn for_public_pda(program_id: &ProgramId, seed: &PdaSeed) -> Self {
|
||||
use risc0_zkvm::sha::{Impl, Sha256 as _};
|
||||
const PROGRAM_DERIVED_ACCOUNT_ID_PREFIX: &[u8; 32] =
|
||||
b"/NSSA/v0.2/AccountId/PDA/\x00\x00\x00\x00\x00\x00\x00";
|
||||
@ -46,9 +48,38 @@ impl From<(&ProgramId, &PdaSeed)> for AccountId {
|
||||
let mut bytes = [0; 96];
|
||||
bytes[0..32].copy_from_slice(PROGRAM_DERIVED_ACCOUNT_ID_PREFIX);
|
||||
let program_id_bytes: &[u8] =
|
||||
bytemuck::try_cast_slice(value.0).expect("ProgramId should be castable to &[u8]");
|
||||
bytemuck::try_cast_slice(program_id).expect("ProgramId should be castable to &[u8]");
|
||||
bytes[32..64].copy_from_slice(program_id_bytes);
|
||||
bytes[64..].copy_from_slice(&value.1.0);
|
||||
bytes[64..].copy_from_slice(&seed.0);
|
||||
Self::new(
|
||||
Impl::hash_bytes(&bytes)
|
||||
.as_bytes()
|
||||
.try_into()
|
||||
.expect("Hash output must be exactly 32 bytes long"),
|
||||
)
|
||||
}
|
||||
|
||||
/// Derives an [`AccountId`] for a private PDA from the program ID, seed, and nullifier
|
||||
/// public key.
|
||||
///
|
||||
/// Unlike public PDAs ([`AccountId::for_public_pda`]), this includes the `npk` in the
|
||||
/// derivation, making the address unique per group of controllers sharing viewing keys.
|
||||
#[must_use]
|
||||
pub fn for_private_pda(
|
||||
program_id: &ProgramId,
|
||||
seed: &PdaSeed,
|
||||
npk: &NullifierPublicKey,
|
||||
) -> Self {
|
||||
use risc0_zkvm::sha::{Impl, Sha256 as _};
|
||||
const PRIVATE_PDA_PREFIX: &[u8; 32] = b"/LEE/v0.3/AccountId/PrivatePDA/\x00";
|
||||
|
||||
let mut bytes = [0_u8; 128];
|
||||
bytes[0..32].copy_from_slice(PRIVATE_PDA_PREFIX);
|
||||
let program_id_bytes: &[u8] =
|
||||
bytemuck::try_cast_slice(program_id).expect("ProgramId should be castable to &[u8]");
|
||||
bytes[32..64].copy_from_slice(program_id_bytes);
|
||||
bytes[64..96].copy_from_slice(&seed.0);
|
||||
bytes[96..128].copy_from_slice(&npk.to_byte_array());
|
||||
Self::new(
|
||||
Impl::hash_bytes(&bytes)
|
||||
.as_bytes()
|
||||
@ -65,6 +96,9 @@ pub struct ChainedCall {
|
||||
pub pre_states: Vec<AccountWithMetadata>,
|
||||
/// The instruction data to pass.
|
||||
pub instruction_data: InstructionData,
|
||||
/// PDA seeds authorized for the callee. For each seed, the callee is authorized to
|
||||
/// mutate the `AccountId` derived from `(caller_program_id, seed)`, regardless of
|
||||
/// whether the account is public or private.
|
||||
pub pda_seeds: Vec<PdaSeed>,
|
||||
}
|
||||
|
||||
@ -114,7 +148,9 @@ pub enum Claim {
|
||||
/// This will give no error if program had authorization in pre state and may be useful
|
||||
/// if program decides to give up authorization for a chained call.
|
||||
Authorized,
|
||||
/// The program requests ownership of the account through a PDA.
|
||||
/// The program requests ownership of the account through a PDA. The program emits the
|
||||
/// seed; the `AccountId` is derived from `(program_id, seed)`, regardless of whether the
|
||||
/// account is public or private.
|
||||
Pda(PdaSeed),
|
||||
}
|
||||
|
||||
@ -382,8 +418,8 @@ impl ProgramOutput {
|
||||
}
|
||||
|
||||
/// Representation of a number as `lo + hi * 2^128`.
|
||||
#[derive(PartialEq, Eq)]
|
||||
struct WrappedBalanceSum {
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct WrappedBalanceSum {
|
||||
lo: u128,
|
||||
hi: u128,
|
||||
}
|
||||
@ -393,7 +429,7 @@ impl WrappedBalanceSum {
|
||||
///
|
||||
/// Returns [`None`] if balance sum overflows `lo + hi * 2^128` representation, which is not
|
||||
/// expected in practical scenarios.
|
||||
fn from_balances(balances: impl Iterator<Item = u128>) -> Option<Self> {
|
||||
pub fn from_balances(balances: impl Iterator<Item = u128>) -> Option<Self> {
|
||||
let mut wrapped = Self { lo: 0, hi: 0 };
|
||||
|
||||
for balance in balances {
|
||||
@ -408,19 +444,93 @@ impl WrappedBalanceSum {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for WrappedBalanceSum {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.hi == 0 {
|
||||
write!(f, "{}", self.lo)
|
||||
} else {
|
||||
write!(f, "{} * 2^128 + {}", self.hi, self.lo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u128> for WrappedBalanceSum {
|
||||
fn from(value: u128) -> Self {
|
||||
Self { lo: value, hi: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ExecutionValidationError {
|
||||
#[error("Pre-state account IDs are not unique")]
|
||||
PreStateAccountIdsNotUnique,
|
||||
|
||||
#[error(
|
||||
"Pre-state and post-state lengths do not match: pre-state length {pre_state_length}, post-state length {post_state_length}"
|
||||
)]
|
||||
MismatchedPreStatePostStateLength {
|
||||
pre_state_length: usize,
|
||||
post_state_length: usize,
|
||||
},
|
||||
|
||||
#[error("Unallowed modification of nonce for account {account_id}")]
|
||||
ModifiedNonce { account_id: AccountId },
|
||||
|
||||
#[error("Unallowed modification of program owner for account {account_id}")]
|
||||
ModifiedProgramOwner { account_id: AccountId },
|
||||
|
||||
#[error(
|
||||
"Trying to decrease balance of account {account_id} owned by {owner_program_id:?} in a program {executing_program_id:?} which is not the owner"
|
||||
)]
|
||||
UnauthorizedBalanceDecrease {
|
||||
account_id: AccountId,
|
||||
owner_program_id: ProgramId,
|
||||
executing_program_id: ProgramId,
|
||||
},
|
||||
|
||||
#[error(
|
||||
"Unauthorized modification of data for account {account_id} which is not default and not owned by executing program {executing_program_id:?}"
|
||||
)]
|
||||
UnauthorizedDataModification {
|
||||
account_id: AccountId,
|
||||
executing_program_id: ProgramId,
|
||||
},
|
||||
|
||||
#[error(
|
||||
"Post-state for account {account_id} has default program owner but pre-state was not default"
|
||||
)]
|
||||
NonDefaultAccountWithDefaultOwner { account_id: AccountId },
|
||||
|
||||
#[error("Total balance across accounts overflowed 2^256 - 1")]
|
||||
BalanceSumOverflow,
|
||||
|
||||
#[error(
|
||||
"Total balance across accounts is not preserved: total balance in pre-states {total_balance_pre_states}, total balance in post-states {total_balance_post_states}"
|
||||
)]
|
||||
MismatchedTotalBalance {
|
||||
total_balance_pre_states: WrappedBalanceSum,
|
||||
total_balance_post_states: WrappedBalanceSum,
|
||||
},
|
||||
}
|
||||
|
||||
/// Computes the set of public-PDA `AccountId`s the callee is authorized to mutate.
|
||||
///
|
||||
/// Returns only public-form derivations, suitable for contexts where all accounts are public
|
||||
/// (e.g. the public-execution path). The privacy circuit must additionally check each mask-3
|
||||
/// `pre_state` against [`AccountId::for_private_pda`] with the supplied npk for that
|
||||
/// `pre_state`.
|
||||
#[must_use]
|
||||
pub fn compute_authorized_pdas(
|
||||
pub fn compute_public_authorized_pdas(
|
||||
caller_program_id: Option<ProgramId>,
|
||||
pda_seeds: &[PdaSeed],
|
||||
) -> HashSet<AccountId> {
|
||||
caller_program_id
|
||||
.map(|caller_program_id| {
|
||||
pda_seeds
|
||||
.iter()
|
||||
.map(|pda_seed| AccountId::from((&caller_program_id, pda_seed)))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
let Some(caller) = caller_program_id else {
|
||||
return HashSet::new();
|
||||
};
|
||||
pda_seeds
|
||||
.iter()
|
||||
.map(|seed| AccountId::for_public_pda(&caller, seed))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Reads the NSSA inputs from the guest environment.
|
||||
@ -448,31 +558,39 @@ pub fn read_nssa_inputs<T: DeserializeOwned>() -> (ProgramInput<T>, InstructionD
|
||||
/// - `pre_states`: The list of input accounts, each annotated with authorization metadata.
|
||||
/// - `post_states`: The list of resulting accounts after executing the program logic.
|
||||
/// - `executing_program_id`: The identifier of the program that was executed.
|
||||
#[must_use]
|
||||
pub fn validate_execution(
|
||||
pre_states: &[AccountWithMetadata],
|
||||
post_states: &[AccountPostState],
|
||||
executing_program_id: ProgramId,
|
||||
) -> bool {
|
||||
) -> Result<(), ExecutionValidationError> {
|
||||
// 1. Check account ids are all different
|
||||
if !validate_uniqueness_of_account_ids(pre_states) {
|
||||
return false;
|
||||
return Err(ExecutionValidationError::PreStateAccountIdsNotUnique);
|
||||
}
|
||||
|
||||
// 2. Lengths must match
|
||||
if pre_states.len() != post_states.len() {
|
||||
return false;
|
||||
return Err(
|
||||
ExecutionValidationError::MismatchedPreStatePostStateLength {
|
||||
pre_state_length: pre_states.len(),
|
||||
post_state_length: post_states.len(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
for (pre, post) in pre_states.iter().zip(post_states) {
|
||||
// 3. Nonce must remain unchanged
|
||||
if pre.account.nonce != post.account.nonce {
|
||||
return false;
|
||||
return Err(ExecutionValidationError::ModifiedNonce {
|
||||
account_id: pre.account_id,
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Program ownership changes are not allowed
|
||||
if pre.account.program_owner != post.account.program_owner {
|
||||
return false;
|
||||
return Err(ExecutionValidationError::ModifiedProgramOwner {
|
||||
account_id: pre.account_id,
|
||||
});
|
||||
}
|
||||
|
||||
let account_program_owner = pre.account.program_owner;
|
||||
@ -481,7 +599,11 @@ pub fn validate_execution(
|
||||
if post.account.balance < pre.account.balance
|
||||
&& account_program_owner != executing_program_id
|
||||
{
|
||||
return false;
|
||||
return Err(ExecutionValidationError::UnauthorizedBalanceDecrease {
|
||||
account_id: pre.account_id,
|
||||
owner_program_id: account_program_owner,
|
||||
executing_program_id,
|
||||
});
|
||||
}
|
||||
|
||||
// 6. Data changes only allowed if owned by executing program or if account pre state has
|
||||
@ -490,13 +612,20 @@ pub fn validate_execution(
|
||||
&& pre.account != Account::default()
|
||||
&& account_program_owner != executing_program_id
|
||||
{
|
||||
return false;
|
||||
return Err(ExecutionValidationError::UnauthorizedDataModification {
|
||||
account_id: pre.account_id,
|
||||
executing_program_id,
|
||||
});
|
||||
}
|
||||
|
||||
// 7. If a post state has default program owner, the pre state must have been a default
|
||||
// account
|
||||
if post.account.program_owner == DEFAULT_PROGRAM_ID && pre.account != Account::default() {
|
||||
return false;
|
||||
return Err(
|
||||
ExecutionValidationError::NonDefaultAccountWithDefaultOwner {
|
||||
account_id: pre.account_id,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -505,20 +634,23 @@ pub fn validate_execution(
|
||||
let Some(total_balance_pre_states) =
|
||||
WrappedBalanceSum::from_balances(pre_states.iter().map(|pre| pre.account.balance))
|
||||
else {
|
||||
return false;
|
||||
return Err(ExecutionValidationError::BalanceSumOverflow);
|
||||
};
|
||||
|
||||
let Some(total_balance_post_states) =
|
||||
WrappedBalanceSum::from_balances(post_states.iter().map(|post| post.account.balance))
|
||||
else {
|
||||
return false;
|
||||
return Err(ExecutionValidationError::BalanceSumOverflow);
|
||||
};
|
||||
|
||||
if total_balance_pre_states != total_balance_post_states {
|
||||
return false;
|
||||
return Err(ExecutionValidationError::MismatchedTotalBalance {
|
||||
total_balance_pre_states,
|
||||
total_balance_post_states,
|
||||
});
|
||||
}
|
||||
|
||||
true
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_uniqueness_of_account_ids(pre_states: &[AccountWithMetadata]) -> bool {
|
||||
@ -709,4 +841,108 @@ mod tests {
|
||||
assert_eq!(account_post_state.account(), &account);
|
||||
assert_eq!(account_post_state.account_mut(), &mut account);
|
||||
}
|
||||
|
||||
// ---- AccountId::for_private_pda tests ----
|
||||
|
||||
/// Pins `AccountId::for_private_pda` against a hardcoded expected output for a specific
|
||||
/// `(program_id, seed, npk)` triple. Any change to `PRIVATE_PDA_PREFIX`, byte ordering,
|
||||
/// or the underlying hash breaks this test.
|
||||
#[test]
|
||||
fn for_private_pda_matches_pinned_value() {
|
||||
let program_id: ProgramId = [1; 8];
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let npk = NullifierPublicKey([3; 32]);
|
||||
let expected = AccountId::new([
|
||||
132, 198, 103, 173, 244, 211, 188, 217, 249, 99, 126, 205, 152, 120, 192, 47, 13, 53,
|
||||
133, 3, 17, 69, 92, 243, 140, 94, 182, 211, 218, 75, 215, 45,
|
||||
]);
|
||||
assert_eq!(
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk),
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
/// Two groups with different viewing keys at the same (program, seed) get different addresses.
|
||||
#[test]
|
||||
fn for_private_pda_differs_for_different_npk() {
|
||||
let program_id: ProgramId = [1; 8];
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let npk_a = NullifierPublicKey([3; 32]);
|
||||
let npk_b = NullifierPublicKey([4; 32]);
|
||||
assert_ne!(
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk_a),
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk_b),
|
||||
);
|
||||
}
|
||||
|
||||
/// Different seeds produce different addresses, even with the same program and npk.
|
||||
#[test]
|
||||
fn for_private_pda_differs_for_different_seed() {
|
||||
let program_id: ProgramId = [1; 8];
|
||||
let seed_a = PdaSeed::new([2; 32]);
|
||||
let seed_b = PdaSeed::new([5; 32]);
|
||||
let npk = NullifierPublicKey([3; 32]);
|
||||
assert_ne!(
|
||||
AccountId::for_private_pda(&program_id, &seed_a, &npk),
|
||||
AccountId::for_private_pda(&program_id, &seed_b, &npk),
|
||||
);
|
||||
}
|
||||
|
||||
/// Different programs produce different addresses, even with the same seed and npk.
|
||||
#[test]
|
||||
fn for_private_pda_differs_for_different_program_id() {
|
||||
let program_id_a: ProgramId = [1; 8];
|
||||
let program_id_b: ProgramId = [9; 8];
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let npk = NullifierPublicKey([3; 32]);
|
||||
assert_ne!(
|
||||
AccountId::for_private_pda(&program_id_a, &seed, &npk),
|
||||
AccountId::for_private_pda(&program_id_b, &seed, &npk),
|
||||
);
|
||||
}
|
||||
|
||||
/// A private PDA at the same (program, seed) has a different address than a public PDA,
|
||||
/// because the private formula uses a different prefix and includes npk.
|
||||
#[test]
|
||||
fn for_private_pda_differs_from_public_pda() {
|
||||
let program_id: ProgramId = [1; 8];
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let npk = NullifierPublicKey([3; 32]);
|
||||
let private_id = AccountId::for_private_pda(&program_id, &seed, &npk);
|
||||
let public_id = AccountId::for_public_pda(&program_id, &seed);
|
||||
assert_ne!(private_id, public_id);
|
||||
}
|
||||
|
||||
/// A private PDA address differs from a standard private account address at the same `npk`,
|
||||
/// because the private PDA formula includes `program_id` and `seed`.
|
||||
#[test]
|
||||
fn for_private_pda_differs_from_standard_private() {
|
||||
let program_id: ProgramId = [1; 8];
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let npk = NullifierPublicKey([3; 32]);
|
||||
let private_pda_id = AccountId::for_private_pda(&program_id, &seed, &npk);
|
||||
let standard_private_id = AccountId::from(&npk);
|
||||
assert_ne!(private_pda_id, standard_private_id);
|
||||
}
|
||||
|
||||
// ---- compute_public_authorized_pdas tests ----
|
||||
|
||||
/// `compute_public_authorized_pdas` returns the public PDA addresses for the caller's seeds.
|
||||
#[test]
|
||||
fn compute_public_authorized_pdas_with_seeds() {
|
||||
let caller: ProgramId = [1; 8];
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let result = compute_public_authorized_pdas(Some(caller), &[seed]);
|
||||
let expected = AccountId::for_public_pda(&caller, &seed);
|
||||
assert!(result.contains(&expected));
|
||||
assert_eq!(result.len(), 1);
|
||||
}
|
||||
|
||||
/// With no caller (top-level call), the result is always empty.
|
||||
#[test]
|
||||
fn compute_public_authorized_pdas_no_caller_returns_empty() {
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let result = compute_public_authorized_pdas(None, &[seed]);
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,16 @@
|
||||
use std::io;
|
||||
|
||||
use nssa_core::{
|
||||
account::{Account, AccountId},
|
||||
program::ProgramId,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! ensure {
|
||||
($cond:expr, $err:expr) => {
|
||||
if !$cond {
|
||||
return Err($err);
|
||||
return Err($err.into());
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -17,7 +21,7 @@ pub enum NssaError {
|
||||
InvalidInput(String),
|
||||
|
||||
#[error("Program violated execution rules")]
|
||||
InvalidProgramBehavior,
|
||||
InvalidProgramBehavior(#[from] InvalidProgramBehaviorError),
|
||||
|
||||
#[error("Serialization error: {0}")]
|
||||
InstructionSerializationError(String),
|
||||
@ -32,15 +36,15 @@ pub enum NssaError {
|
||||
InvalidPublicKey(#[source] k256::schnorr::Error),
|
||||
|
||||
#[error("Invalid hex for public key")]
|
||||
InvalidHexPublicKey(hex::FromHexError),
|
||||
InvalidHexPublicKey(#[source] hex::FromHexError),
|
||||
|
||||
#[error("Risc0 error: {0}")]
|
||||
#[error("Failed to write program input: {0}")]
|
||||
ProgramWriteInputFailed(String),
|
||||
|
||||
#[error("Risc0 error: {0}")]
|
||||
#[error("Failed to execute program: {0}")]
|
||||
ProgramExecutionFailed(String),
|
||||
|
||||
#[error("Risc0 error: {0}")]
|
||||
#[error("Failed to prove program: {0}")]
|
||||
ProgramProveFailed(String),
|
||||
|
||||
#[error("Invalid transaction: {0}")]
|
||||
@ -77,6 +81,61 @@ pub enum NssaError {
|
||||
OutOfValidityWindow,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum InvalidProgramBehaviorError {
|
||||
#[error(
|
||||
"Inconsistent pre-state for account {account_id} : expected {expected:?}, actual {actual:?}"
|
||||
)]
|
||||
InconsistentAccountPreState {
|
||||
account_id: AccountId,
|
||||
// Boxed to reduce the size of the error type
|
||||
expected: Box<Account>,
|
||||
actual: Box<Account>,
|
||||
},
|
||||
|
||||
#[error(
|
||||
"Inconsistent authorization for account {account_id} : expected {expected_authorization}, actual {actual_authorization}"
|
||||
)]
|
||||
InconsistentAccountAuthorization {
|
||||
account_id: AccountId,
|
||||
expected_authorization: bool,
|
||||
actual_authorization: bool,
|
||||
},
|
||||
|
||||
#[error("Program ID mismatch: expected {expected:?}, actual {actual:?}")]
|
||||
MismatchedProgramId {
|
||||
expected: ProgramId,
|
||||
actual: ProgramId,
|
||||
},
|
||||
|
||||
#[error("Caller program ID mismatch: expected {expected:?}, actual {actual:?}")]
|
||||
MismatchedCallerProgramId {
|
||||
expected: Option<ProgramId>,
|
||||
actual: Option<ProgramId>,
|
||||
},
|
||||
|
||||
#[error(transparent)]
|
||||
ExecutionValidationFailed(#[from] nssa_core::program::ExecutionValidationError),
|
||||
|
||||
#[error("Trying to claim account {account_id} which is not default")]
|
||||
ClaimedNonDefaultAccount { account_id: AccountId },
|
||||
|
||||
#[error("Trying to claim account {account_id} which is not authorized")]
|
||||
ClaimedUnauthorizedAccount { account_id: AccountId },
|
||||
|
||||
#[error("PDA claim mismatch: expected {expected:?}, actual {actual:?}")]
|
||||
MismatchedPdaClaim {
|
||||
expected: AccountId,
|
||||
actual: AccountId,
|
||||
},
|
||||
|
||||
#[error("Default account {account_id} was modified without being claimed")]
|
||||
DefaultAccountModifiedWithoutClaim { account_id: AccountId },
|
||||
|
||||
#[error("Called program {program_id:?} which is not listed in dependencies")]
|
||||
UndeclaredProgramDependency { program_id: ProgramId },
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ use nssa_core::{
|
||||
use risc0_zkvm::{ExecutorEnv, InnerReceipt, ProverOpts, Receipt, default_prover};
|
||||
|
||||
use crate::{
|
||||
error::NssaError,
|
||||
error::{InvalidProgramBehaviorError, NssaError},
|
||||
program::Program,
|
||||
program_methods::{PRIVACY_PRESERVING_CIRCUIT_ELF, PRIVACY_PRESERVING_CIRCUIT_ID},
|
||||
state::MAX_NUMBER_CHAINED_CALLS,
|
||||
@ -113,9 +113,11 @@ pub fn execute_and_prove(
|
||||
env_builder.add_assumption(inner_receipt);
|
||||
|
||||
for new_call in program_output.chained_calls.into_iter().rev() {
|
||||
let next_program = dependencies
|
||||
.get(&new_call.program_id)
|
||||
.ok_or(NssaError::InvalidProgramBehavior)?;
|
||||
let next_program = dependencies.get(&new_call.program_id).ok_or(
|
||||
InvalidProgramBehaviorError::UndeclaredProgramDependency {
|
||||
program_id: new_call.program_id,
|
||||
},
|
||||
)?;
|
||||
chained_calls.push_front((new_call, next_program, Some(chained_call.program_id)));
|
||||
}
|
||||
|
||||
|
||||
@ -292,6 +292,36 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn pda_claimer() -> Self {
|
||||
use test_program_methods::{PDA_CLAIMER_ELF, PDA_CLAIMER_ID};
|
||||
|
||||
Self {
|
||||
id: PDA_CLAIMER_ID,
|
||||
elf: PDA_CLAIMER_ELF.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn private_pda_delegator() -> Self {
|
||||
use test_program_methods::{PRIVATE_PDA_DELEGATOR_ELF, PRIVATE_PDA_DELEGATOR_ID};
|
||||
|
||||
Self {
|
||||
id: PRIVATE_PDA_DELEGATOR_ID,
|
||||
elf: PRIVATE_PDA_DELEGATOR_ELF.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn two_pda_claimer() -> Self {
|
||||
use test_program_methods::{TWO_PDA_CLAIMER_ELF, TWO_PDA_CLAIMER_ID};
|
||||
|
||||
Self {
|
||||
id: TWO_PDA_CLAIMER_ID,
|
||||
elf: TWO_PDA_CLAIMER_ELF.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn changer_claimer() -> Self {
|
||||
use test_program_methods::{CHANGER_CLAIMER_ELF, CHANGER_CLAIMER_ID};
|
||||
@ -312,6 +342,16 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn auth_asserting_noop() -> Self {
|
||||
use test_program_methods::{AUTH_ASSERTING_NOOP_ELF, AUTH_ASSERTING_NOOP_ID};
|
||||
|
||||
Self {
|
||||
id: AUTH_ASSERTING_NOOP_ID,
|
||||
elf: AUTH_ASSERTING_NOOP_ELF.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn malicious_authorization_changer() -> Self {
|
||||
use test_program_methods::{
|
||||
|
||||
@ -366,12 +366,15 @@ pub mod tests {
|
||||
Timestamp,
|
||||
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
|
||||
encryption::{EphemeralPublicKey, Scalar, ViewingPublicKey},
|
||||
program::{BlockValidityWindow, PdaSeed, ProgramId, TimestampValidityWindow},
|
||||
program::{
|
||||
BlockValidityWindow, ExecutionValidationError, PdaSeed, ProgramId,
|
||||
TimestampValidityWindow, WrappedBalanceSum,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
PublicKey, PublicTransaction, V03State,
|
||||
error::NssaError,
|
||||
error::{InvalidProgramBehaviorError, NssaError},
|
||||
execute_and_prove,
|
||||
privacy_preserving_transaction::{
|
||||
PrivacyPreservingTransaction,
|
||||
@ -933,10 +936,11 @@ pub mod tests {
|
||||
|
||||
#[test]
|
||||
fn program_should_fail_if_modifies_nonces() {
|
||||
let initial_data = [(AccountId::new([1; 32]), 100)];
|
||||
let account_id = AccountId::new([1; 32]);
|
||||
let initial_data = [(account_id, 100)];
|
||||
let mut state =
|
||||
V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs();
|
||||
let account_ids = vec![AccountId::new([1; 32])];
|
||||
let account_ids = vec![account_id];
|
||||
let program_id = Program::nonce_changer_program().id();
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, account_ids, vec![], ()).unwrap();
|
||||
@ -945,7 +949,14 @@ pub mod tests {
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(NssaError::InvalidProgramBehavior(
|
||||
InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::ModifiedNonce { account_id: err_account_id }
|
||||
)
|
||||
)) if err_account_id == account_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -962,7 +973,17 @@ pub mod tests {
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(NssaError::InvalidProgramBehavior(
|
||||
InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::MismatchedPreStatePostStateLength {
|
||||
pre_state_length,
|
||||
post_state_length
|
||||
}
|
||||
)
|
||||
)) if pre_state_length == 1 && post_state_length == 2
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -979,7 +1000,17 @@ pub mod tests {
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(NssaError::InvalidProgramBehavior(
|
||||
InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::MismatchedPreStatePostStateLength {
|
||||
pre_state_length,
|
||||
post_state_length
|
||||
}
|
||||
)
|
||||
)) if pre_state_length == 2 && post_state_length == 1
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1003,7 +1034,12 @@ pub mod tests {
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(NssaError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::ModifiedProgramOwner { account_id: err_account_id }
|
||||
))) if err_account_id == account_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1027,7 +1063,12 @@ pub mod tests {
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(NssaError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::ModifiedProgramOwner { account_id: err_account_id }
|
||||
))) if err_account_id == account_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1051,7 +1092,12 @@ pub mod tests {
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(NssaError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::ModifiedProgramOwner { account_id: err_account_id }
|
||||
))) if err_account_id == account_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1075,16 +1121,21 @@ pub mod tests {
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(NssaError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::ModifiedProgramOwner { account_id: err_account_id }
|
||||
))) if err_account_id == account_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_should_fail_if_transfers_balance_from_non_owned_account() {
|
||||
let initial_data = [(AccountId::new([1; 32]), 100)];
|
||||
let mut state =
|
||||
V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs();
|
||||
let sender_account_id = AccountId::new([1; 32]);
|
||||
let receiver_account_id = AccountId::new([2; 32]);
|
||||
let initial_data = [(sender_account_id, 100)];
|
||||
let mut state =
|
||||
V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs();
|
||||
let balance_to_move: u128 = 1;
|
||||
let program_id = Program::simple_balance_transfer().id();
|
||||
assert_ne!(
|
||||
@ -1103,7 +1154,12 @@ pub mod tests {
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(NssaError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::UnauthorizedBalanceDecrease { account_id: err_account_id, owner_program_id, executing_program_id }
|
||||
))) if err_account_id == sender_account_id && owner_program_id != program_id && executing_program_id == program_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1128,7 +1184,12 @@ pub mod tests {
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(NssaError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::UnauthorizedDataModification { account_id: err_account_id, executing_program_id }
|
||||
))) if err_account_id == account_id && executing_program_id == program_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1146,7 +1207,12 @@ pub mod tests {
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(NssaError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::MismatchedTotalBalance { total_balance_pre_states, total_balance_post_states }
|
||||
))) if total_balance_pre_states == 0.into() && total_balance_post_states == 1.into()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1175,7 +1241,12 @@ pub mod tests {
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(NssaError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::MismatchedTotalBalance { total_balance_pre_states, total_balance_post_states }
|
||||
))) if total_balance_pre_states == 100.into() && total_balance_post_states == 99.into()
|
||||
));
|
||||
}
|
||||
|
||||
fn test_public_account_keys_1() -> TestPublicKeys {
|
||||
@ -2243,9 +2314,16 @@ pub mod tests {
|
||||
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
/// A mask-3 account that no program claims via `Claim::Pda` and no caller authorizes via
|
||||
/// `ChainedCall.pda_seeds` has no binding between its supplied npk and its `account_id`,
|
||||
/// so the circuit must reject. Here `simple_balance_transfer` emits no claim for the
|
||||
/// second account, leaving position 1 unbound.
|
||||
#[test]
|
||||
fn circuit_should_fail_with_invalid_visibility_mask_value() {
|
||||
fn private_pda_without_binding_fails() {
|
||||
let program = Program::simple_balance_transfer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk());
|
||||
let public_account_1 = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
@ -2255,17 +2333,235 @@ pub mod tests {
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
);
|
||||
let public_account_2 =
|
||||
let private_pda_account =
|
||||
AccountWithMetadata::new(Account::default(), false, AccountId::new([1; 32]));
|
||||
|
||||
let visibility_mask = [0, 3];
|
||||
let result = execute_and_prove(
|
||||
vec![public_account_1, public_account_2],
|
||||
vec![public_account_1, private_pda_account],
|
||||
Program::serialize_instruction(10_u128).unwrap(),
|
||||
visibility_mask.to_vec(),
|
||||
vec![(npk, shared_secret)],
|
||||
vec![],
|
||||
vec![None],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
/// Happy path: a program claims a new mask-3 account via `Claim::Pda(seed)`. The circuit
|
||||
/// reads the npk for that `pre_state` from `private_account_keys` at the `pre_state`'s
|
||||
/// position, derives `AccountId` via `AccountId::for_private_pda(program_id, seed, npk)`, and
|
||||
/// asserts it equals the `pre_state`'s `account_id`. The equality both validates the claim
|
||||
/// and binds the supplied npk to the `account_id`.
|
||||
#[test]
|
||||
fn private_pda_claim_succeeds() {
|
||||
let program = Program::pda_claimer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk());
|
||||
|
||||
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk);
|
||||
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pre_state],
|
||||
Program::serialize_instruction(seed).unwrap(),
|
||||
vec![3],
|
||||
vec![(npk, shared_secret)],
|
||||
vec![],
|
||||
vec![None],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
let (output, _proof) = result.expect("mask-3 private PDA claim should succeed");
|
||||
assert_eq!(output.new_nullifiers.len(), 1);
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
assert_eq!(output.ciphertexts.len(), 1);
|
||||
assert!(output.public_pre_states.is_empty());
|
||||
assert!(output.public_post_states.is_empty());
|
||||
}
|
||||
|
||||
/// An npk is supplied that does not match the `pre_state`'s `account_id` under
|
||||
/// `AccountId::for_private_pda(program, claim_seed, npk)`. The claim equality check rejects.
|
||||
#[test]
|
||||
fn private_pda_npk_mismatch_fails() {
|
||||
// `keys_a` produces the `pre_state`'s `account_id` (the registered pair), `keys_b` is
|
||||
// the mismatched pair supplied in `private_account_keys` for that pre_state.
|
||||
let program = Program::pda_claimer();
|
||||
let keys_a = test_private_account_keys_1();
|
||||
let keys_b = test_private_account_keys_2();
|
||||
let npk_a = keys_a.npk();
|
||||
let npk_b = keys_b.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let shared_secret = SharedSecretKey::new(&[55; 32], &keys_b.vpk());
|
||||
|
||||
// `account_id` is derived from `npk_a`, but `npk_b` is supplied for this pre_state.
|
||||
// `AccountId::for_private_pda(program, seed, npk_b) != account_id`, so the claim check in
|
||||
// the circuit must reject.
|
||||
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk_a);
|
||||
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pre_state],
|
||||
Program::serialize_instruction(seed).unwrap(),
|
||||
vec![3],
|
||||
vec![(npk_b, shared_secret)],
|
||||
vec![],
|
||||
vec![None],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
/// Happy path for the caller-seeds authorization of a mask-3 PDA. The delegator claims a
|
||||
/// private PDA via `Claim::Pda(seed)`, then chains to a callee (`noop`) delegating the same
|
||||
/// seed via `ChainedCall.pda_seeds`. In the callee's step, the `pre_state`'s authorization
|
||||
/// is established via the private derivation
|
||||
/// `AccountId::for_private_pda(delegator, seed, npk) == pre.account_id`.
|
||||
#[test]
|
||||
fn caller_pda_seeds_authorize_private_pda_for_callee() {
|
||||
let delegator = Program::private_pda_delegator();
|
||||
let callee = Program::auth_asserting_noop();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([77; 32]);
|
||||
let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk());
|
||||
|
||||
let account_id = AccountId::for_private_pda(&delegator.id(), &seed, &npk);
|
||||
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());
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pre_state],
|
||||
Program::serialize_instruction((seed, seed, callee_id)).unwrap(),
|
||||
vec![3],
|
||||
vec![(npk, shared_secret)],
|
||||
vec![],
|
||||
vec![None],
|
||||
&program_with_deps,
|
||||
);
|
||||
|
||||
let (output, _proof) =
|
||||
result.expect("caller-seeds authorization of mask-3 private PDA should succeed");
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
assert_eq!(output.new_nullifiers.len(), 1);
|
||||
}
|
||||
|
||||
/// The delegator chains with a different seed than the one it claimed with. In the callee
|
||||
/// step, neither public nor private caller-seeds authorization matches; `pre.is_authorized`
|
||||
/// was set to `true` by the delegator but no proven source supports it, so the consistency
|
||||
/// assertion rejects.
|
||||
#[test]
|
||||
fn caller_pda_seeds_with_wrong_seed_rejects_private_pda_for_callee() {
|
||||
let delegator = Program::private_pda_delegator();
|
||||
let callee = Program::auth_asserting_noop();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let claim_seed = PdaSeed::new([77; 32]);
|
||||
let wrong_delegated_seed = PdaSeed::new([88; 32]);
|
||||
let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk());
|
||||
|
||||
let account_id = AccountId::for_private_pda(&delegator.id(), &claim_seed, &npk);
|
||||
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());
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pre_state],
|
||||
Program::serialize_instruction((claim_seed, wrong_delegated_seed, callee_id)).unwrap(),
|
||||
vec![3],
|
||||
vec![(npk, shared_secret)],
|
||||
vec![],
|
||||
vec![None],
|
||||
&program_with_deps,
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(NssaError::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` (mask-3, `alice_npk`) and
|
||||
/// `PDA_bob` (mask-3, `bob_npk`) under the same seed in one transaction, and once reuse
|
||||
/// is supported a later chained call could delegate both to a callee via
|
||||
/// `pda_seeds: [S]` and mix balances across them. The binding check rejects the setup
|
||||
/// here: after the first claim records `(program, seed) → PDA_alice`, the second claim
|
||||
/// tries to record `(program, seed) → PDA_bob` and panics.
|
||||
#[test]
|
||||
fn two_private_pda_claims_under_same_seed_are_rejected() {
|
||||
let program = Program::two_pda_claimer();
|
||||
let keys_a = test_private_account_keys_1();
|
||||
let keys_b = test_private_account_keys_2();
|
||||
let seed = PdaSeed::new([55; 32]);
|
||||
let shared_a = SharedSecretKey::new(&[66; 32], &keys_a.vpk());
|
||||
let shared_b = SharedSecretKey::new(&[77; 32], &keys_b.vpk());
|
||||
|
||||
let account_a = AccountId::for_private_pda(&program.id(), &seed, &keys_a.npk());
|
||||
let account_b = AccountId::for_private_pda(&program.id(), &seed, &keys_b.npk());
|
||||
|
||||
let pre_a = AccountWithMetadata::new(Account::default(), false, account_a);
|
||||
let pre_b = AccountWithMetadata::new(Account::default(), false, account_b);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pre_a, pre_b],
|
||||
Program::serialize_instruction(seed).unwrap(),
|
||||
vec![3, 3],
|
||||
vec![(keys_a.npk(), shared_a), (keys_b.npk(), shared_b)],
|
||||
vec![],
|
||||
vec![None, None],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
/// Pins the current limitation: a mask-3 PDA that was claimed in a previous transaction
|
||||
/// cannot be re-used in a new transaction as-is. This PR only binds supplied npks via a
|
||||
/// fresh `Claim::Pda` or a caller's `ChainedCall.pda_seeds`, neither is present when a
|
||||
/// program operates on an already-owned private PDA at top level. The reject site is the
|
||||
/// post-loop `private_pda_bound_positions` assertion in
|
||||
/// `privacy_preserving_circuit.rs`: `noop` emits no `Claim::Pda` and there is no caller
|
||||
/// `ChainedCall.pda_seeds`, so position 0 is never bound and the assertion fires.
|
||||
// TODO: a follow-up PR in the Private PDAs series needs to let the wallet supply a
|
||||
// `(seed, original_owner_program_id)` side input per mask-3 `pre_state` so the circuit
|
||||
// can re-verify `AccountId::for_private_pda(owner, seed, npk) == pre.account_id` without a
|
||||
// claim.
|
||||
#[test]
|
||||
fn private_pda_top_level_reuse_rejected_by_binding_check() {
|
||||
let program = Program::noop();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk());
|
||||
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);
|
||||
let owned_pre_state = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
account_id,
|
||||
);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![owned_pre_state],
|
||||
Program::serialize_instruction(()).unwrap(),
|
||||
vec![3],
|
||||
vec![(npk, shared_secret)],
|
||||
vec![],
|
||||
vec![None],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
@ -2671,7 +2967,7 @@ pub mod tests {
|
||||
fn execution_that_requires_authentication_of_a_program_derived_account_id_succeeds() {
|
||||
let chain_caller = Program::chain_caller();
|
||||
let pda_seed = PdaSeed::new([37; 32]);
|
||||
let from = AccountId::from((&chain_caller.id(), &pda_seed));
|
||||
let from = AccountId::for_public_pda(&chain_caller.id(), &pda_seed);
|
||||
let to = AccountId::new([2; 32]);
|
||||
let initial_balance = 1000;
|
||||
let initial_data = [(from, initial_balance), (to, 0)];
|
||||
@ -2983,7 +3279,8 @@ pub mod tests {
|
||||
let pinata_definition_id = AccountId::new([1; 32]);
|
||||
let pinata_token_definition_id = AccountId::new([2; 32]);
|
||||
// Total supply of pinata token will be in an account under a PDA.
|
||||
let pinata_token_holding_id = AccountId::from((&pinata_token.id(), &PdaSeed::new([0; 32])));
|
||||
let pinata_token_holding_id =
|
||||
AccountId::for_public_pda(&pinata_token.id(), &PdaSeed::new([0; 32]));
|
||||
let winner_token_holding_id = AccountId::new([3; 32]);
|
||||
|
||||
let expected_winner_account_holding = token_core::TokenHolding::Fungible {
|
||||
@ -3088,7 +3385,12 @@ pub mod tests {
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(NssaError::InvalidProgramBehavior(
|
||||
InvalidProgramBehaviorError::ClaimedNonDefaultAccount { account_id: err_account_id }
|
||||
)) if err_account_id == account_id
|
||||
));
|
||||
}
|
||||
|
||||
/// This test ensures that even if a malicious program tries to perform overflow of balances
|
||||
@ -3134,7 +3436,22 @@ pub mod tests {
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&sender_key]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
let res = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
assert!(matches!(res, Err(NssaError::InvalidProgramBehavior)));
|
||||
let expected_total_balance_pre_states = WrappedBalanceSum::from_balances(
|
||||
[sender_init_balance, recipient_init_balance].into_iter(),
|
||||
)
|
||||
.unwrap();
|
||||
let expected_total_balance_post_states = WrappedBalanceSum::from_balances(
|
||||
[sender_init_balance, recipient_init_balance, u128::MAX, 1].into_iter(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
res,
|
||||
Err(NssaError::InvalidProgramBehavior(
|
||||
InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::MismatchedTotalBalance { total_balance_pre_states, total_balance_post_states }
|
||||
)
|
||||
)) if total_balance_pre_states == expected_total_balance_pre_states && total_balance_post_states == expected_total_balance_post_states
|
||||
));
|
||||
|
||||
let sender_post = state.get_account_by_id(sender_id);
|
||||
let recipient_post = state.get_account_by_id(recipient_id);
|
||||
@ -3379,7 +3696,14 @@ pub mod tests {
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
// Should fail - cannot modify data without claiming the account
|
||||
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(NssaError::InvalidProgramBehavior(
|
||||
InvalidProgramBehaviorError::DefaultAccountModifiedWithoutClaim {
|
||||
account_id: err_account_id
|
||||
}
|
||||
)) if err_account_id == account_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -3973,8 +4297,8 @@ pub mod tests {
|
||||
let callback = Program::flash_swap_callback();
|
||||
let token = Program::authenticated_transfer_program();
|
||||
|
||||
let vault_id = AccountId::from((&initiator.id(), &PdaSeed::new([0_u8; 32])));
|
||||
let receiver_id = AccountId::from((&callback.id(), &PdaSeed::new([1_u8; 32])));
|
||||
let vault_id = AccountId::for_public_pda(&initiator.id(), &PdaSeed::new([0_u8; 32]));
|
||||
let receiver_id = AccountId::for_public_pda(&callback.id(), &PdaSeed::new([1_u8; 32]));
|
||||
|
||||
let initial_balance: u128 = 1000;
|
||||
let amount_out: u128 = 100;
|
||||
@ -4024,8 +4348,8 @@ pub mod tests {
|
||||
let callback = Program::flash_swap_callback();
|
||||
let token = Program::authenticated_transfer_program();
|
||||
|
||||
let vault_id = AccountId::from((&initiator.id(), &PdaSeed::new([0_u8; 32])));
|
||||
let receiver_id = AccountId::from((&callback.id(), &PdaSeed::new([1_u8; 32])));
|
||||
let vault_id = AccountId::for_public_pda(&initiator.id(), &PdaSeed::new([0_u8; 32]));
|
||||
let receiver_id = AccountId::for_public_pda(&callback.id(), &PdaSeed::new([1_u8; 32]));
|
||||
|
||||
let initial_balance: u128 = 1000;
|
||||
let amount_out: u128 = 100;
|
||||
@ -4082,8 +4406,8 @@ pub mod tests {
|
||||
let callback = Program::flash_swap_callback();
|
||||
let token = Program::authenticated_transfer_program();
|
||||
|
||||
let vault_id = AccountId::from((&initiator.id(), &PdaSeed::new([0_u8; 32])));
|
||||
let receiver_id = AccountId::from((&callback.id(), &PdaSeed::new([1_u8; 32])));
|
||||
let vault_id = AccountId::for_public_pda(&initiator.id(), &PdaSeed::new([0_u8; 32]));
|
||||
let receiver_id = AccountId::for_public_pda(&callback.id(), &PdaSeed::new([1_u8; 32]));
|
||||
|
||||
let initial_balance: u128 = 1000;
|
||||
|
||||
@ -4131,7 +4455,7 @@ pub mod tests {
|
||||
let initiator = Program::flash_swap_initiator();
|
||||
let token = Program::authenticated_transfer_program();
|
||||
|
||||
let vault_id = AccountId::from((&initiator.id(), &PdaSeed::new([0_u8; 32])));
|
||||
let vault_id = AccountId::for_public_pda(&initiator.id(), &PdaSeed::new([0_u8; 32]));
|
||||
|
||||
let vault_account = Account {
|
||||
program_owner: token.id(),
|
||||
|
||||
@ -8,13 +8,13 @@ use nssa_core::{
|
||||
BlockId, Commitment, Nullifier, PrivacyPreservingCircuitOutput, Timestamp,
|
||||
account::{Account, AccountId, AccountWithMetadata},
|
||||
program::{
|
||||
ChainedCall, Claim, DEFAULT_PROGRAM_ID, compute_authorized_pdas, validate_execution,
|
||||
ChainedCall, Claim, DEFAULT_PROGRAM_ID, compute_public_authorized_pdas, validate_execution,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
V03State, ensure,
|
||||
error::NssaError,
|
||||
error::{InvalidProgramBehaviorError, NssaError},
|
||||
privacy_preserving_transaction::{
|
||||
PrivacyPreservingTransaction, circuit::Proof, message::Message,
|
||||
},
|
||||
@ -129,7 +129,7 @@ impl ValidatedStateDiff {
|
||||
);
|
||||
|
||||
let authorized_pdas =
|
||||
compute_authorized_pdas(caller_program_id, &chained_call.pda_seeds);
|
||||
compute_public_authorized_pdas(caller_program_id, &chained_call.pda_seeds);
|
||||
|
||||
let is_authorized = |account_id: &AccountId| {
|
||||
signer_account_ids.contains(account_id) || authorized_pdas.contains(account_id)
|
||||
@ -145,39 +145,52 @@ impl ValidatedStateDiff {
|
||||
.unwrap_or_else(|| state.get_account_by_id(account_id));
|
||||
ensure!(
|
||||
pre.account == expected_pre,
|
||||
NssaError::InvalidProgramBehavior
|
||||
InvalidProgramBehaviorError::InconsistentAccountPreState {
|
||||
account_id,
|
||||
expected: Box::new(expected_pre),
|
||||
actual: Box::new(pre.account.clone())
|
||||
}
|
||||
);
|
||||
|
||||
// Check that authorization flags are consistent with the provided ones or
|
||||
// authorized by program through the PDA mechanism
|
||||
let expected_is_authorized = is_authorized(&account_id);
|
||||
ensure!(
|
||||
pre.is_authorized == is_authorized(&account_id),
|
||||
NssaError::InvalidProgramBehavior
|
||||
pre.is_authorized == expected_is_authorized,
|
||||
InvalidProgramBehaviorError::InconsistentAccountAuthorization {
|
||||
account_id,
|
||||
expected_authorization: expected_is_authorized,
|
||||
actual_authorization: pre.is_authorized
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Verify that the program output's self_program_id matches the expected program ID.
|
||||
ensure!(
|
||||
program_output.self_program_id == chained_call.program_id,
|
||||
NssaError::InvalidProgramBehavior
|
||||
InvalidProgramBehaviorError::MismatchedProgramId {
|
||||
expected: chained_call.program_id,
|
||||
actual: program_output.self_program_id
|
||||
}
|
||||
);
|
||||
|
||||
// Verify that the program output's caller_program_id matches the actual caller.
|
||||
ensure!(
|
||||
program_output.caller_program_id == caller_program_id,
|
||||
NssaError::InvalidProgramBehavior
|
||||
InvalidProgramBehaviorError::MismatchedCallerProgramId {
|
||||
expected: caller_program_id,
|
||||
actual: program_output.caller_program_id,
|
||||
}
|
||||
);
|
||||
|
||||
// Verify execution corresponds to a well-behaved program.
|
||||
// See the # Programs section for the definition of the `validate_execution` method.
|
||||
ensure!(
|
||||
validate_execution(
|
||||
&program_output.pre_states,
|
||||
&program_output.post_states,
|
||||
chained_call.program_id,
|
||||
),
|
||||
NssaError::InvalidProgramBehavior
|
||||
);
|
||||
validate_execution(
|
||||
&program_output.pre_states,
|
||||
&program_output.post_states,
|
||||
chained_call.program_id,
|
||||
)
|
||||
.map_err(InvalidProgramBehaviorError::ExecutionValidationFailed)?;
|
||||
|
||||
// Verify validity window
|
||||
ensure!(
|
||||
@ -192,27 +205,34 @@ impl ValidatedStateDiff {
|
||||
let Some(claim) = post.required_claim() else {
|
||||
continue;
|
||||
};
|
||||
let account_id = program_output.pre_states[i].account_id;
|
||||
|
||||
// The invoked program can only claim accounts with default program id.
|
||||
ensure!(
|
||||
post.account().program_owner == DEFAULT_PROGRAM_ID,
|
||||
NssaError::InvalidProgramBehavior
|
||||
InvalidProgramBehaviorError::ClaimedNonDefaultAccount { account_id }
|
||||
);
|
||||
|
||||
let account_id = program_output.pre_states[i].account_id;
|
||||
|
||||
match claim {
|
||||
Claim::Authorized => {
|
||||
// The program can only claim accounts that were authorized by the signer.
|
||||
ensure!(
|
||||
is_authorized(&account_id),
|
||||
NssaError::InvalidProgramBehavior
|
||||
InvalidProgramBehaviorError::ClaimedUnauthorizedAccount { account_id }
|
||||
);
|
||||
}
|
||||
Claim::Pda(seed) => {
|
||||
// The program can only claim accounts that correspond to the PDAs it is
|
||||
// authorized to claim.
|
||||
let pda = AccountId::from((&chained_call.program_id, &seed));
|
||||
ensure!(account_id == pda, NssaError::InvalidProgramBehavior);
|
||||
// authorized to claim. The public-execution path only sees public
|
||||
// accounts, so the public-PDA derivation is the correct formula here.
|
||||
let pda = AccountId::for_public_pda(&chained_call.program_id, &seed);
|
||||
ensure!(
|
||||
account_id == pda,
|
||||
InvalidProgramBehaviorError::MismatchedPdaClaim {
|
||||
expected: pda,
|
||||
actual: account_id
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -238,7 +258,7 @@ impl ValidatedStateDiff {
|
||||
}
|
||||
|
||||
// Check that all modified uninitialized accounts where claimed
|
||||
for post in state_diff.iter().filter_map(|(account_id, post)| {
|
||||
for (account_id, post) in state_diff.iter().filter_map(|(account_id, post)| {
|
||||
let pre = state.get_account_by_id(*account_id);
|
||||
if pre.program_owner != DEFAULT_PROGRAM_ID {
|
||||
return None;
|
||||
@ -246,11 +266,11 @@ impl ValidatedStateDiff {
|
||||
if pre == *post {
|
||||
return None;
|
||||
}
|
||||
Some(post)
|
||||
Some((*account_id, post))
|
||||
}) {
|
||||
ensure!(
|
||||
post.program_owner != DEFAULT_PROGRAM_ID,
|
||||
NssaError::InvalidProgramBehavior
|
||||
InvalidProgramBehaviorError::DefaultAccountModifiedWithoutClaim { account_id }
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ use nssa_core::{
|
||||
compute_digest_for_path,
|
||||
program::{
|
||||
AccountPostState, BlockValidityWindow, ChainedCall, Claim, DEFAULT_PROGRAM_ID,
|
||||
MAX_NUMBER_CHAINED_CALLS, ProgramId, ProgramOutput, TimestampValidityWindow,
|
||||
MAX_NUMBER_CHAINED_CALLS, PdaSeed, ProgramId, ProgramOutput, TimestampValidityWindow,
|
||||
validate_execution,
|
||||
},
|
||||
};
|
||||
@ -23,15 +23,62 @@ struct ExecutionState {
|
||||
post_states: HashMap<AccountId, Account>,
|
||||
block_validity_window: BlockValidityWindow,
|
||||
timestamp_validity_window: TimestampValidityWindow,
|
||||
/// Positions (in `pre_states`) of mask-3 accounts whose supplied npk has been bound to
|
||||
/// their `AccountId` via a proven `AccountId::for_private_pda(program_id, seed, npk)`
|
||||
/// check.
|
||||
/// Two proof paths populate this set: a `Claim::Pda(seed)` in a program's `post_state` on
|
||||
/// that `pre_state`, or a caller's `ChainedCall.pda_seeds` entry matching that `pre_state`
|
||||
/// under the private derivation. Binding is an idempotent property, not an event: the same
|
||||
/// position can legitimately be bound through both paths in the same tx (e.g. a program
|
||||
/// claims a private PDA and then delegates it to a callee), and the set uses `contains`,
|
||||
/// not `assert!(insert)`. After the main loop, every mask-3 position must appear in this
|
||||
/// set; otherwise the npk is unbound and the circuit rejects.
|
||||
private_pda_bound_positions: HashSet<usize>,
|
||||
/// Across the whole transaction, each `(program_id, seed)` pair may resolve to at most one
|
||||
/// `AccountId`. A seed under a program can derive a family of accounts, one public PDA and
|
||||
/// one private PDA per distinct npk. Without this check, a single `pda_seeds: [S]` entry in
|
||||
/// a chained call could authorize multiple family members at once (different npks under the
|
||||
/// same seed) and let a callee mix balances across them. Every claim and every
|
||||
/// caller-authorization resolution is recorded here, either as a new `(program, seed)` →
|
||||
/// `AccountId` entry or as an equality check against the existing one, making the rule: one
|
||||
/// `(program, seed)` → one account per tx.
|
||||
pda_family_binding: HashMap<(ProgramId, PdaSeed), AccountId>,
|
||||
/// Map from a mask-3 `pre_state`'s position in `visibility_mask` to the npk supplied for
|
||||
/// that position in `private_account_keys`. Built once in `derive_from_outputs` by walking
|
||||
/// `visibility_mask` in lock-step with `private_account_keys`, used later by the claim and
|
||||
/// caller-seeds authorization paths.
|
||||
private_pda_npk_by_position: HashMap<usize, NullifierPublicKey>,
|
||||
}
|
||||
|
||||
impl ExecutionState {
|
||||
/// Validate program outputs and derive the overall execution state.
|
||||
pub fn derive_from_outputs(
|
||||
visibility_mask: &[u8],
|
||||
private_account_keys: &[(NullifierPublicKey, SharedSecretKey)],
|
||||
program_id: ProgramId,
|
||||
program_outputs: Vec<ProgramOutput>,
|
||||
) -> Self {
|
||||
// Build position → npk map for mask-3 pre_states. `private_account_keys` is consumed in
|
||||
// pre_state order across all masks 1/2/3, so walk `visibility_mask` in lock-step. The
|
||||
// downstream `compute_circuit_output` also consumes the same iterator and its trailing
|
||||
// assertions catch an over-supply of keys; under-supply surfaces here.
|
||||
let mut private_pda_npk_by_position: HashMap<usize, NullifierPublicKey> = HashMap::new();
|
||||
{
|
||||
let mut keys_iter = private_account_keys.iter();
|
||||
for (pos, &mask) in visibility_mask.iter().enumerate() {
|
||||
if matches!(mask, 1..=3) {
|
||||
let (npk, _) = keys_iter.next().unwrap_or_else(|| {
|
||||
panic!(
|
||||
"private_account_keys shorter than visibility_mask demands: no key for masked position {pos} (mask {mask})"
|
||||
)
|
||||
});
|
||||
if mask == 3 {
|
||||
private_pda_npk_by_position.insert(pos, *npk);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let block_valid_from = program_outputs
|
||||
.iter()
|
||||
.filter_map(|output| output.block_validity_window.start())
|
||||
@ -66,6 +113,9 @@ impl ExecutionState {
|
||||
post_states: HashMap::new(),
|
||||
block_validity_window,
|
||||
timestamp_validity_window,
|
||||
private_pda_bound_positions: HashSet::new(),
|
||||
pda_family_binding: HashMap::new(),
|
||||
private_pda_npk_by_position,
|
||||
};
|
||||
|
||||
let Some(first_output) = program_outputs.first() else {
|
||||
@ -125,25 +175,27 @@ impl ExecutionState {
|
||||
|
||||
// Check that the program is well behaved.
|
||||
// See the # Programs section for the definition of the `validate_execution` method.
|
||||
let execution_valid = validate_execution(
|
||||
let validated_execution = validate_execution(
|
||||
&program_output.pre_states,
|
||||
&program_output.post_states,
|
||||
chained_call.program_id,
|
||||
);
|
||||
assert!(execution_valid, "Bad behaved program");
|
||||
if let Err(err) = validated_execution {
|
||||
panic!(
|
||||
"Invalid program behavior in program {:?}: {err}",
|
||||
chained_call.program_id
|
||||
);
|
||||
}
|
||||
|
||||
for next_call in program_output.chained_calls.iter().rev() {
|
||||
chained_calls.push_front((next_call.clone(), Some(chained_call.program_id)));
|
||||
}
|
||||
|
||||
let authorized_pdas = nssa_core::program::compute_authorized_pdas(
|
||||
caller_program_id,
|
||||
&chained_call.pda_seeds,
|
||||
);
|
||||
execution_state.validate_and_sync_states(
|
||||
visibility_mask,
|
||||
chained_call.program_id,
|
||||
&authorized_pdas,
|
||||
caller_program_id,
|
||||
&chained_call.pda_seeds,
|
||||
program_output.pre_states,
|
||||
program_output.post_states,
|
||||
);
|
||||
@ -157,6 +209,19 @@ impl ExecutionState {
|
||||
"Inner call without a chained call found",
|
||||
);
|
||||
|
||||
// Every mask-3 pre_state must have had its npk bound to its account_id, either via a
|
||||
// `Claim::Pda(seed)` in some program's post_state or via a caller's `pda_seeds` matching
|
||||
// the private derivation. An unbound mask-3 pre_state has no cryptographic link between
|
||||
// the supplied npk and the account_id, and must be rejected.
|
||||
for (pos, &mask) in visibility_mask.iter().enumerate() {
|
||||
if mask == 3 {
|
||||
assert!(
|
||||
execution_state.private_pda_bound_positions.contains(&pos),
|
||||
"private PDA pre_state at position {pos} has no proven (seed, npk) binding via Claim::Pda or caller pda_seeds"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check that all modified uninitialized accounts were claimed
|
||||
for (account_id, post) in execution_state
|
||||
.pre_states
|
||||
@ -186,7 +251,8 @@ impl ExecutionState {
|
||||
&mut self,
|
||||
visibility_mask: &[u8],
|
||||
program_id: ProgramId,
|
||||
authorized_pdas: &HashSet<AccountId>,
|
||||
caller_program_id: Option<ProgramId>,
|
||||
caller_pda_seeds: &[PdaSeed],
|
||||
pre_states: Vec<AccountWithMetadata>,
|
||||
post_states: Vec<AccountPostState>,
|
||||
) {
|
||||
@ -213,19 +279,28 @@ impl ExecutionState {
|
||||
"Inconsistent pre state for account {pre_account_id}",
|
||||
);
|
||||
|
||||
let previous_is_authorized = self
|
||||
let (previous_is_authorized, pre_state_position) = self
|
||||
.pre_states
|
||||
.iter()
|
||||
.find(|acc| acc.account_id == pre_account_id)
|
||||
.enumerate()
|
||||
.find(|(_, acc)| acc.account_id == pre_account_id)
|
||||
.map_or_else(
|
||||
|| panic!(
|
||||
"Pre state must exist in execution state for account {pre_account_id}",
|
||||
),
|
||||
|acc| acc.is_authorized
|
||||
|(pos, acc)| (acc.is_authorized, pos)
|
||||
);
|
||||
|
||||
let is_authorized =
|
||||
previous_is_authorized || authorized_pdas.contains(&pre_account_id);
|
||||
let is_authorized = resolve_authorization_and_record_bindings(
|
||||
&mut self.pda_family_binding,
|
||||
&mut self.private_pda_bound_positions,
|
||||
&self.private_pda_npk_by_position,
|
||||
pre_account_id,
|
||||
pre_state_position,
|
||||
caller_program_id,
|
||||
caller_pda_seeds,
|
||||
previous_is_authorized,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
pre_is_authorized, is_authorized,
|
||||
@ -252,9 +327,9 @@ impl ExecutionState {
|
||||
.position(|acc| acc.account_id == pre_account_id)
|
||||
.expect("Pre state must exist at this point");
|
||||
|
||||
let is_public_account = visibility_mask[pre_state_position] == 0;
|
||||
if is_public_account {
|
||||
match claim {
|
||||
let mask = visibility_mask[pre_state_position];
|
||||
match mask {
|
||||
0 => match claim {
|
||||
Claim::Authorized => {
|
||||
// Note: no need to check authorized pdas because we have already
|
||||
// checked consistency of authorization above.
|
||||
@ -264,18 +339,52 @@ impl ExecutionState {
|
||||
);
|
||||
}
|
||||
Claim::Pda(seed) => {
|
||||
let pda = AccountId::from((&program_id, &seed));
|
||||
let pda = AccountId::for_public_pda(&program_id, &seed);
|
||||
assert_eq!(
|
||||
pre_account_id, pda,
|
||||
"Invalid PDA claim for account {pre_account_id} which does not match derived PDA {pda}"
|
||||
);
|
||||
assert_family_binding(
|
||||
&mut self.pda_family_binding,
|
||||
program_id,
|
||||
seed,
|
||||
pre_account_id,
|
||||
);
|
||||
}
|
||||
},
|
||||
3 => {
|
||||
match claim {
|
||||
Claim::Authorized => {
|
||||
assert!(
|
||||
pre_is_authorized,
|
||||
"Cannot claim unauthorized private PDA {pre_account_id}"
|
||||
);
|
||||
}
|
||||
Claim::Pda(seed) => {
|
||||
let npk = self
|
||||
.private_pda_npk_by_position
|
||||
.get(&pre_state_position)
|
||||
.expect("private PDA pre_state must have an npk in the position map");
|
||||
let pda = AccountId::for_private_pda(&program_id, &seed, npk);
|
||||
assert_eq!(
|
||||
pre_account_id, pda,
|
||||
"Invalid private PDA claim for account {pre_account_id}"
|
||||
);
|
||||
self.private_pda_bound_positions.insert(pre_state_position);
|
||||
assert_family_binding(
|
||||
&mut self.pda_family_binding,
|
||||
program_id,
|
||||
seed,
|
||||
pre_account_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We don't care about the exact claim mechanism for private accounts.
|
||||
// This is because the main reason to have it is to protect against PDA griefing
|
||||
// attacks in public execution, while private PDA doesn't make much sense
|
||||
// anyway.
|
||||
_ => {
|
||||
// Mask 1/2: standard private accounts don't enforce the claim semantics.
|
||||
// Unauthorized private claiming is intentionally allowed since operating
|
||||
// these accounts requires the npk/nsk keypair anyway.
|
||||
}
|
||||
}
|
||||
|
||||
post.account_mut().program_owner = program_id;
|
||||
@ -299,6 +408,82 @@ impl ExecutionState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Record or re-verify the `(program_id, seed) → account_id` family binding for the
|
||||
/// transaction. Any claim or caller-seed authorization that resolves a `pre_state` under
|
||||
/// `(program_id, seed)` must agree with every prior resolution of the same pair; otherwise a
|
||||
/// single `pda_seeds: [seed]` entry could authorize multiple private-PDA family members at
|
||||
/// once (different npks under the same seed) and let a callee mix balances across them. Free
|
||||
/// function so callers can pass `&mut self.pda_family_binding` without holding a borrow on
|
||||
/// the surrounding struct's other fields.
|
||||
fn assert_family_binding(
|
||||
bindings: &mut HashMap<(ProgramId, PdaSeed), AccountId>,
|
||||
program_id: ProgramId,
|
||||
seed: PdaSeed,
|
||||
account_id: AccountId,
|
||||
) {
|
||||
match bindings.entry((program_id, seed)) {
|
||||
Entry::Vacant(e) => {
|
||||
e.insert(account_id);
|
||||
}
|
||||
Entry::Occupied(e) => {
|
||||
assert_eq!(
|
||||
*e.get(),
|
||||
account_id,
|
||||
"Two different accounts resolved under the same (program, seed) in one transaction: existing {}, new {account_id}",
|
||||
e.get()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve the authorization state of a `pre_state` seen again in a chained call and record
|
||||
/// any resulting bindings. Returns `true` if the `pre_state` is authorized through either a
|
||||
/// previously-seen authorization or a matching caller seed (under the public or private
|
||||
/// derivation). 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.
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "breaking out a context struct does not buy us anything here"
|
||||
)]
|
||||
fn resolve_authorization_and_record_bindings(
|
||||
pda_family_binding: &mut HashMap<(ProgramId, PdaSeed), AccountId>,
|
||||
private_pda_bound_positions: &mut HashSet<usize>,
|
||||
private_pda_npk_by_position: &HashMap<usize, NullifierPublicKey>,
|
||||
pre_account_id: AccountId,
|
||||
pre_state_position: usize,
|
||||
caller_program_id: Option<ProgramId>,
|
||||
caller_pda_seeds: &[PdaSeed],
|
||||
previous_is_authorized: bool,
|
||||
) -> bool {
|
||||
let matched_caller_seed: Option<(PdaSeed, bool, ProgramId)> =
|
||||
caller_program_id.and_then(|caller| {
|
||||
caller_pda_seeds.iter().find_map(|seed| {
|
||||
if AccountId::for_public_pda(&caller, seed) == pre_account_id {
|
||||
return Some((*seed, false, caller));
|
||||
}
|
||||
if let Some(npk) = private_pda_npk_by_position.get(&pre_state_position)
|
||||
&& AccountId::for_private_pda(&caller, seed, npk) == pre_account_id
|
||||
{
|
||||
return Some((*seed, true, caller));
|
||||
}
|
||||
None
|
||||
})
|
||||
});
|
||||
|
||||
if let Some((seed, is_private_form, caller)) = matched_caller_seed {
|
||||
assert_family_binding(pda_family_binding, caller, seed, pre_account_id);
|
||||
if is_private_form {
|
||||
private_pda_bound_positions.insert(pre_state_position);
|
||||
}
|
||||
}
|
||||
|
||||
previous_is_authorized || matched_caller_seed.is_some()
|
||||
}
|
||||
|
||||
fn compute_circuit_output(
|
||||
execution_state: ExecutionState,
|
||||
visibility_mask: &[u8],
|
||||
@ -434,6 +619,88 @@ fn compute_circuit_output(
|
||||
.checked_add(1)
|
||||
.unwrap_or_else(|| panic!("Too many private accounts, output index overflow"));
|
||||
}
|
||||
3 => {
|
||||
// Private PDA account. The supplied npk has already been bound to
|
||||
// `pre_state.account_id` upstream in `validate_and_sync_states`, either via a
|
||||
// `Claim::Pda(seed)` match or via a caller `pda_seeds` match, both of which
|
||||
// assert `AccountId::for_private_pda(owner, seed, npk) == account_id`. The
|
||||
// post-loop assertion in `derive_from_outputs` (see the
|
||||
// `private_pda_bound_positions` check) guarantees that every mask-3
|
||||
// position has been through at least one such binding, so this
|
||||
// branch can safely use the wallet npk without re-verifying.
|
||||
let Some((npk, shared_secret)) = private_keys_iter.next() else {
|
||||
panic!("Missing private account key");
|
||||
};
|
||||
|
||||
let (new_nullifier, new_nonce) = if pre_state.is_authorized {
|
||||
// Existing private PDA with authentication (like mask 1)
|
||||
let Some(nsk) = private_nsks_iter.next() else {
|
||||
panic!("Missing private account nullifier secret key");
|
||||
};
|
||||
assert_eq!(
|
||||
npk,
|
||||
&NullifierPublicKey::from(nsk),
|
||||
"Nullifier public key mismatch"
|
||||
);
|
||||
|
||||
let Some(membership_proof_opt) = private_membership_proofs_iter.next() else {
|
||||
panic!("Missing membership proof");
|
||||
};
|
||||
|
||||
let new_nullifier = compute_nullifier_and_set_digest(
|
||||
membership_proof_opt.as_ref(),
|
||||
&pre_state.account,
|
||||
npk,
|
||||
nsk,
|
||||
);
|
||||
let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk);
|
||||
(new_nullifier, new_nonce)
|
||||
} else {
|
||||
// New private PDA (like mask 2). The default + unauthorized requirement
|
||||
// here rules out use cases like a fully-private multisig, which would need
|
||||
// a non-default, non-authorized private PDA input account.
|
||||
// TODO(private-pdas-pr-2/3): relax this once the wallet can supply a
|
||||
// `(seed, owner)` side input so the npk-to-account_id binding can be
|
||||
// re-verified for an existing private PDA without a `Claim::Pda` or caller
|
||||
// `pda_seeds` match.
|
||||
assert_eq!(
|
||||
pre_state.account,
|
||||
Account::default(),
|
||||
"New private PDA must be default"
|
||||
);
|
||||
|
||||
let Some(membership_proof_opt) = private_membership_proofs_iter.next() else {
|
||||
panic!("Missing membership proof");
|
||||
};
|
||||
assert!(
|
||||
membership_proof_opt.is_none(),
|
||||
"Membership proof must be None for new accounts"
|
||||
);
|
||||
|
||||
let nullifier = Nullifier::for_account_initialization(npk);
|
||||
let new_nonce = Nonce::private_account_nonce_init(npk);
|
||||
((nullifier, DUMMY_COMMITMENT_HASH), new_nonce)
|
||||
};
|
||||
output.new_nullifiers.push(new_nullifier);
|
||||
|
||||
let mut post_with_updated_nonce = post_state;
|
||||
post_with_updated_nonce.nonce = new_nonce;
|
||||
|
||||
let commitment_post = Commitment::new(npk, &post_with_updated_nonce);
|
||||
|
||||
let encrypted_account = EncryptionScheme::encrypt(
|
||||
&post_with_updated_nonce,
|
||||
shared_secret,
|
||||
&commitment_post,
|
||||
output_index,
|
||||
);
|
||||
|
||||
output.new_commitments.push(commitment_post);
|
||||
output.ciphertexts.push(encrypted_account);
|
||||
output_index = output_index
|
||||
.checked_add(1)
|
||||
.unwrap_or_else(|| panic!("Too many private accounts, output index overflow"));
|
||||
}
|
||||
_ => panic!("Invalid visibility mask value"),
|
||||
}
|
||||
}
|
||||
@ -496,8 +763,12 @@ fn main() {
|
||||
program_id,
|
||||
} = env::read();
|
||||
|
||||
let execution_state =
|
||||
ExecutionState::derive_from_outputs(&visibility_mask, program_id, program_outputs);
|
||||
let execution_state = ExecutionState::derive_from_outputs(
|
||||
&visibility_mask,
|
||||
&private_account_keys,
|
||||
program_id,
|
||||
program_outputs,
|
||||
);
|
||||
|
||||
let output = compute_circuit_output(
|
||||
execution_state,
|
||||
|
||||
@ -135,10 +135,10 @@ pub fn compute_pool_pda(
|
||||
definition_token_a_id: AccountId,
|
||||
definition_token_b_id: AccountId,
|
||||
) -> AccountId {
|
||||
AccountId::from((
|
||||
AccountId::for_public_pda(
|
||||
&amm_program_id,
|
||||
&compute_pool_pda_seed(definition_token_a_id, definition_token_b_id),
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
@ -175,10 +175,10 @@ pub fn compute_vault_pda(
|
||||
pool_id: AccountId,
|
||||
definition_token_id: AccountId,
|
||||
) -> AccountId {
|
||||
AccountId::from((
|
||||
AccountId::for_public_pda(
|
||||
&amm_program_id,
|
||||
&compute_vault_pda_seed(pool_id, definition_token_id),
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
@ -199,7 +199,7 @@ pub fn compute_vault_pda_seed(pool_id: AccountId, definition_token_id: AccountId
|
||||
|
||||
#[must_use]
|
||||
pub fn compute_liquidity_token_pda(amm_program_id: ProgramId, pool_id: AccountId) -> AccountId {
|
||||
AccountId::from((&amm_program_id, &compute_liquidity_token_pda_seed(pool_id)))
|
||||
AccountId::for_public_pda(&amm_program_id, &compute_liquidity_token_pda_seed(pool_id))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
|
||||
@ -61,7 +61,7 @@ pub fn compute_ata_seed(owner_id: AccountId, definition_id: AccountId) -> PdaSee
|
||||
}
|
||||
|
||||
pub fn get_associated_token_account_id(ata_program_id: &ProgramId, seed: &PdaSeed) -> AccountId {
|
||||
AccountId::from((ata_program_id, seed))
|
||||
AccountId::for_public_pda(ata_program_id, seed)
|
||||
}
|
||||
|
||||
/// Verify the ATA's address matches `(ata_program_id, owner, definition)` and return
|
||||
|
||||
@ -1094,7 +1094,7 @@ mod tests {
|
||||
// Start a sequencer from config with a preconfigured private genesis account
|
||||
let mut config = setup_sequencer_config();
|
||||
config.initial_private_accounts = Some(vec![PrivateAccountPublicInitialData {
|
||||
npk: npk.clone(),
|
||||
npk,
|
||||
account: genesis_account,
|
||||
}]);
|
||||
|
||||
@ -1110,7 +1110,7 @@ mod tests {
|
||||
vec![AccountWithMetadata::new(Account::default(), true, &npk)],
|
||||
Program::serialize_instruction(0_u128).unwrap(),
|
||||
vec![1],
|
||||
vec![(npk.clone(), shared_secret)],
|
||||
vec![(npk, shared_secret)],
|
||||
vec![nsk],
|
||||
vec![None],
|
||||
&Program::authenticated_transfer_program().into(),
|
||||
|
||||
40
test_program_methods/guest/src/bin/auth_asserting_noop.rs
Normal file
40
test_program_methods/guest/src/bin/auth_asserting_noop.rs
Normal file
@ -0,0 +1,40 @@
|
||||
use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs};
|
||||
|
||||
/// A variant of `noop` that asserts every `pre_state.is_authorized == true` before echoing
|
||||
/// the `post_states`. Any unauthorized `pre_state` panics the guest, failing the whole
|
||||
/// circuit proof. Used as a callee in private-PDA delegation tests to actually exercise the
|
||||
/// authorization propagated through `ChainedCall.pda_seeds`.
|
||||
type Instruction = ();
|
||||
|
||||
fn main() {
|
||||
let (
|
||||
ProgramInput {
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
..
|
||||
},
|
||||
instruction_words,
|
||||
) = read_nssa_inputs::<Instruction>();
|
||||
|
||||
for pre in &pre_states {
|
||||
assert!(
|
||||
pre.is_authorized,
|
||||
"auth_asserting_noop: pre_state {} is not authorized",
|
||||
pre.account_id
|
||||
);
|
||||
}
|
||||
|
||||
let post_states = pre_states
|
||||
.iter()
|
||||
.map(|account| AccountPostState::new(account.account.clone()))
|
||||
.collect();
|
||||
ProgramOutput::new(
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
instruction_words,
|
||||
pre_states,
|
||||
post_states,
|
||||
)
|
||||
.write();
|
||||
}
|
||||
32
test_program_methods/guest/src/bin/pda_claimer.rs
Normal file
32
test_program_methods/guest/src/bin/pda_claimer.rs
Normal file
@ -0,0 +1,32 @@
|
||||
use nssa_core::program::{
|
||||
AccountPostState, Claim, PdaSeed, ProgramInput, ProgramOutput, read_nssa_inputs,
|
||||
};
|
||||
|
||||
type Instruction = PdaSeed;
|
||||
|
||||
fn main() {
|
||||
let (
|
||||
ProgramInput {
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
instruction: seed,
|
||||
},
|
||||
instruction_words,
|
||||
) = read_nssa_inputs::<Instruction>();
|
||||
|
||||
let Ok([pre]) = <[_; 1]>::try_from(pre_states) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let account_post = AccountPostState::new_claimed(pre.account.clone(), Claim::Pda(seed));
|
||||
|
||||
ProgramOutput::new(
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
instruction_words,
|
||||
vec![pre],
|
||||
vec![account_post],
|
||||
)
|
||||
.write();
|
||||
}
|
||||
51
test_program_methods/guest/src/bin/private_pda_delegator.rs
Normal file
51
test_program_methods/guest/src/bin/private_pda_delegator.rs
Normal file
@ -0,0 +1,51 @@
|
||||
use nssa_core::program::{
|
||||
AccountPostState, ChainedCall, Claim, PdaSeed, ProgramId, ProgramInput, ProgramOutput,
|
||||
read_nssa_inputs,
|
||||
};
|
||||
use risc0_zkvm::serde::to_vec;
|
||||
|
||||
/// Claims the sole `pre_state` as a PDA with `claim_seed`, then chains to `callee_program_id`
|
||||
/// delegating authorization with `delegated_seed` in `pda_seeds`. When `claim_seed ==
|
||||
/// delegated_seed` this exercises the happy caller-seeds authorization path for mask-3 private
|
||||
/// PDAs in `validate_and_sync_states`; when they differ, the callee's mask-3 `pre_state` has
|
||||
/// no matching authorization source and the circuit must reject.
|
||||
type Instruction = (PdaSeed, PdaSeed, ProgramId);
|
||||
|
||||
fn main() {
|
||||
let (
|
||||
ProgramInput {
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
instruction: (claim_seed, delegated_seed, callee_program_id),
|
||||
},
|
||||
instruction_words,
|
||||
) = read_nssa_inputs::<Instruction>();
|
||||
|
||||
let Ok([pre]) = <[_; 1]>::try_from(pre_states) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let claimed = AccountPostState::new_claimed(pre.account.clone(), Claim::Pda(claim_seed));
|
||||
|
||||
let mut pre_for_callee = pre.clone();
|
||||
pre_for_callee.is_authorized = true;
|
||||
pre_for_callee.account.program_owner = self_program_id;
|
||||
|
||||
let chained_call = ChainedCall {
|
||||
program_id: callee_program_id,
|
||||
instruction_data: to_vec(&()).unwrap(),
|
||||
pre_states: vec![pre_for_callee],
|
||||
pda_seeds: vec![delegated_seed],
|
||||
};
|
||||
|
||||
ProgramOutput::new(
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
instruction_words,
|
||||
vec![pre],
|
||||
vec![claimed],
|
||||
)
|
||||
.with_chained_calls(vec![chained_call])
|
||||
.write();
|
||||
}
|
||||
37
test_program_methods/guest/src/bin/two_pda_claimer.rs
Normal file
37
test_program_methods/guest/src/bin/two_pda_claimer.rs
Normal file
@ -0,0 +1,37 @@
|
||||
use nssa_core::program::{
|
||||
AccountPostState, Claim, PdaSeed, ProgramInput, ProgramOutput, read_nssa_inputs,
|
||||
};
|
||||
|
||||
/// Claims two `pre_states` under the same `seed`. Used to exercise the tx-wide
|
||||
/// `(program_id, seed) → AccountId` family-binding check: when both `pre_states` are mask-3
|
||||
/// with different npks, each `Claim::Pda(seed)` resolves to a different `AccountId` under the
|
||||
/// same `(program, seed)` key, and the circuit must reject.
|
||||
type Instruction = PdaSeed;
|
||||
|
||||
fn main() {
|
||||
let (
|
||||
ProgramInput {
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
instruction: seed,
|
||||
},
|
||||
instruction_words,
|
||||
) = read_nssa_inputs::<Instruction>();
|
||||
|
||||
let Ok([pre_a, pre_b]) = <[_; 2]>::try_from(pre_states) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let claim_a = AccountPostState::new_claimed(pre_a.account.clone(), Claim::Pda(seed));
|
||||
let claim_b = AccountPostState::new_claimed(pre_b.account.clone(), Claim::Pda(seed));
|
||||
|
||||
ProgramOutput::new(
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
instruction_words,
|
||||
vec![pre_a, pre_b],
|
||||
vec![claim_a, claim_b],
|
||||
)
|
||||
.write();
|
||||
}
|
||||
@ -169,7 +169,7 @@ pub fn initial_commitments() -> Vec<PrivateAccountPublicInitialData> {
|
||||
initial_priv_accounts_private_keys()
|
||||
.into_iter()
|
||||
.map(|data| PrivateAccountPublicInitialData {
|
||||
npk: data.key_chain.nullifier_public_key.clone(),
|
||||
npk: data.key_chain.nullifier_public_key,
|
||||
account: data.account,
|
||||
})
|
||||
.collect()
|
||||
|
||||
@ -393,7 +393,7 @@ impl WalletCore {
|
||||
acc_manager.visibility_mask().to_vec(),
|
||||
private_account_keys
|
||||
.iter()
|
||||
.map(|keys| (keys.npk.clone(), keys.ssk))
|
||||
.map(|keys| (keys.npk, keys.ssk))
|
||||
.collect::<Vec<_>>(),
|
||||
acc_manager.private_account_auth(),
|
||||
acc_manager.private_account_membership_proofs(),
|
||||
@ -407,7 +407,7 @@ impl WalletCore {
|
||||
Vec::from_iter(acc_manager.public_account_nonces()),
|
||||
private_account_keys
|
||||
.iter()
|
||||
.map(|keys| (keys.npk.clone(), keys.vpk.clone(), keys.epk.clone()))
|
||||
.map(|keys| (keys.npk, keys.vpk.clone(), keys.epk.clone()))
|
||||
.collect(),
|
||||
output,
|
||||
)
|
||||
|
||||
@ -59,7 +59,7 @@ impl WalletCore {
|
||||
&nssa::program::Program::serialize_instruction(solution).unwrap(),
|
||||
&[0, 1],
|
||||
&produce_random_nonces(1),
|
||||
&[(winner_npk.clone(), shared_secret_winner.clone())],
|
||||
&[(winner_npk, shared_secret_winner.clone())],
|
||||
&[(winner_nsk.unwrap())],
|
||||
&[winner_proof],
|
||||
&program.into(),
|
||||
@ -71,7 +71,7 @@ impl WalletCore {
|
||||
vec![pinata_account_id],
|
||||
vec![],
|
||||
vec![(
|
||||
winner_npk.clone(),
|
||||
winner_npk,
|
||||
winner_vpk.clone(),
|
||||
eph_holder_winner.generate_ephemeral_public_key(),
|
||||
)],
|
||||
@ -126,7 +126,7 @@ impl WalletCore {
|
||||
&nssa::program::Program::serialize_instruction(solution).unwrap(),
|
||||
&[0, 2],
|
||||
&produce_random_nonces(1),
|
||||
&[(winner_npk.clone(), shared_secret_winner.clone())],
|
||||
&[(winner_npk, shared_secret_winner.clone())],
|
||||
&[],
|
||||
&[],
|
||||
&program.into(),
|
||||
@ -138,7 +138,7 @@ impl WalletCore {
|
||||
vec![pinata_account_id],
|
||||
vec![],
|
||||
vec![(
|
||||
winner_npk.clone(),
|
||||
winner_npk,
|
||||
winner_vpk.clone(),
|
||||
eph_holder_winner.generate_ephemeral_public_key(),
|
||||
)],
|
||||
|
||||
@ -138,7 +138,7 @@ impl AccountManager {
|
||||
let eph_holder = EphemeralKeyHolder::new(&pre.npk);
|
||||
|
||||
Some(PrivateAccountKeys {
|
||||
npk: pre.npk.clone(),
|
||||
npk: pre.npk,
|
||||
ssk: eph_holder.calculate_shared_secret_sender(&pre.vpk),
|
||||
vpk: pre.vpk.clone(),
|
||||
epk: eph_holder.generate_ephemeral_public_key(),
|
||||
|
||||
@ -108,8 +108,8 @@ impl WalletCore {
|
||||
&[1, 1],
|
||||
&produce_random_nonces(2),
|
||||
&[
|
||||
(from_npk.clone(), shared_secret_from.clone()),
|
||||
(to_npk.clone(), shared_secret_to.clone()),
|
||||
(from_npk, shared_secret_from.clone()),
|
||||
(to_npk, shared_secret_to.clone()),
|
||||
],
|
||||
&[
|
||||
(from_nsk.unwrap(), from_proof.unwrap()),
|
||||
@ -124,12 +124,12 @@ impl WalletCore {
|
||||
vec![],
|
||||
vec![
|
||||
(
|
||||
from_npk.clone(),
|
||||
from_npk,
|
||||
from_vpk.clone(),
|
||||
eph_holder_from.generate_ephemeral_public_key(),
|
||||
),
|
||||
(
|
||||
to_npk.clone(),
|
||||
to_npk,
|
||||
to_vpk.clone(),
|
||||
eph_holder_to.generate_ephemeral_public_key(),
|
||||
),
|
||||
@ -185,8 +185,8 @@ impl WalletCore {
|
||||
&[1, 2],
|
||||
&produce_random_nonces(2),
|
||||
&[
|
||||
(from_npk.clone(), shared_secret_from.clone()),
|
||||
(to_npk.clone(), shared_secret_to.clone()),
|
||||
(from_npk, shared_secret_from.clone()),
|
||||
(to_npk, shared_secret_to.clone()),
|
||||
],
|
||||
&[(from_nsk.unwrap(), from_proof.unwrap())],
|
||||
&program.into(),
|
||||
@ -198,12 +198,12 @@ impl WalletCore {
|
||||
vec![],
|
||||
vec![
|
||||
(
|
||||
from_npk.clone(),
|
||||
from_npk,
|
||||
from_vpk.clone(),
|
||||
eph_holder_from.generate_ephemeral_public_key(),
|
||||
),
|
||||
(
|
||||
to_npk.clone(),
|
||||
to_npk,
|
||||
to_vpk.clone(),
|
||||
eph_holder_to.generate_ephemeral_public_key(),
|
||||
),
|
||||
@ -255,8 +255,8 @@ impl WalletCore {
|
||||
&[1, 2],
|
||||
&produce_random_nonces(2),
|
||||
&[
|
||||
(from_npk.clone(), shared_secret_from.clone()),
|
||||
(to_npk.clone(), shared_secret_to.clone()),
|
||||
(from_npk, shared_secret_from.clone()),
|
||||
(to_npk, shared_secret_to.clone()),
|
||||
],
|
||||
&[(from_nsk.unwrap(), from_proof.unwrap())],
|
||||
&program.into(),
|
||||
@ -268,12 +268,12 @@ impl WalletCore {
|
||||
vec![],
|
||||
vec![
|
||||
(
|
||||
from_npk.clone(),
|
||||
from_npk,
|
||||
from_vpk.clone(),
|
||||
eph_holder.generate_ephemeral_public_key(),
|
||||
),
|
||||
(
|
||||
to_npk.clone(),
|
||||
to_npk,
|
||||
to_vpk.clone(),
|
||||
eph_holder.generate_ephemeral_public_key(),
|
||||
),
|
||||
@ -324,7 +324,7 @@ impl WalletCore {
|
||||
&instruction_data,
|
||||
&[1, 0],
|
||||
&produce_random_nonces(1),
|
||||
&[(from_npk.clone(), shared_secret.clone())],
|
||||
&[(from_npk, shared_secret.clone())],
|
||||
&[(from_nsk.unwrap(), from_proof.unwrap())],
|
||||
&program.into(),
|
||||
)
|
||||
@ -334,7 +334,7 @@ impl WalletCore {
|
||||
vec![to],
|
||||
vec![],
|
||||
vec![(
|
||||
from_npk.clone(),
|
||||
from_npk,
|
||||
from_vpk.clone(),
|
||||
eph_holder.generate_ephemeral_public_key(),
|
||||
)],
|
||||
@ -385,7 +385,7 @@ impl WalletCore {
|
||||
&instruction_data,
|
||||
&[0, 1],
|
||||
&produce_random_nonces(1),
|
||||
&[(to_npk.clone(), shared_secret.clone())],
|
||||
&[(to_npk, shared_secret.clone())],
|
||||
&[(to_nsk.unwrap(), to_proof)],
|
||||
&program.into(),
|
||||
)
|
||||
@ -395,7 +395,7 @@ impl WalletCore {
|
||||
vec![from],
|
||||
vec![from_acc.nonce],
|
||||
vec![(
|
||||
to_npk.clone(),
|
||||
to_npk,
|
||||
to_vpk.clone(),
|
||||
eph_holder.generate_ephemeral_public_key(),
|
||||
)],
|
||||
@ -451,7 +451,7 @@ impl WalletCore {
|
||||
&instruction_data,
|
||||
&[0, 2],
|
||||
&produce_random_nonces(1),
|
||||
&[(to_npk.clone(), shared_secret.clone())],
|
||||
&[(to_npk, shared_secret.clone())],
|
||||
&[],
|
||||
&program.into(),
|
||||
)
|
||||
@ -461,7 +461,7 @@ impl WalletCore {
|
||||
vec![from],
|
||||
vec![from_acc.nonce],
|
||||
vec![(
|
||||
to_npk.clone(),
|
||||
to_npk,
|
||||
to_vpk.clone(),
|
||||
eph_holder.generate_ephemeral_public_key(),
|
||||
)],
|
||||
@ -513,7 +513,7 @@ impl WalletCore {
|
||||
&instruction_data,
|
||||
&[0, 2],
|
||||
&produce_random_nonces(1),
|
||||
&[(to_npk.clone(), shared_secret.clone())],
|
||||
&[(to_npk, shared_secret.clone())],
|
||||
&[],
|
||||
&program.into(),
|
||||
)
|
||||
@ -523,7 +523,7 @@ impl WalletCore {
|
||||
vec![from],
|
||||
vec![from_acc.nonce],
|
||||
vec![(
|
||||
to_npk.clone(),
|
||||
to_npk,
|
||||
to_vpk.clone(),
|
||||
eph_holder.generate_ephemeral_public_key(),
|
||||
)],
|
||||
@ -565,7 +565,7 @@ impl WalletCore {
|
||||
&Program::serialize_instruction(instruction).unwrap(),
|
||||
&[2],
|
||||
&produce_random_nonces(1),
|
||||
&[(from_npk.clone(), shared_secret_from.clone())],
|
||||
&[(from_npk, shared_secret_from.clone())],
|
||||
&[],
|
||||
&Program::authenticated_transfer_program().into(),
|
||||
)
|
||||
@ -575,7 +575,7 @@ impl WalletCore {
|
||||
vec![],
|
||||
vec![],
|
||||
vec![(
|
||||
from_npk.clone(),
|
||||
from_npk,
|
||||
from_vpk.clone(),
|
||||
eph_holder_from.generate_ephemeral_public_key(),
|
||||
)],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user