Merge pull request #259 from logos-blockchain/arjentix/multi-chain-calls-in-private-tx

Implement private multi chain calls
This commit is contained in:
Daniil Polyakov 2026-01-21 16:55:49 +03:00 committed by GitHub
commit fb62143398
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
45 changed files with 1032 additions and 551 deletions

34
Cargo.lock generated
View File

@ -2668,6 +2668,7 @@ dependencies = [
"secp256k1", "secp256k1",
"serde", "serde",
"sha2", "sha2",
"test-case",
"test_program_methods", "test_program_methods",
"thiserror", "thiserror",
] ]
@ -4315,6 +4316,39 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "test-case"
version = "3.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8"
dependencies = [
"test-case-macros",
]
[[package]]
name = "test-case-core"
version = "3.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f"
dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"syn 2.0.111",
]
[[package]]
name = "test-case-macros"
version = "3.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
"test-case-core",
]
[[package]] [[package]]
name = "test_program_methods" name = "test_program_methods"
version = "0.1.0" version = "0.1.0"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -50,7 +50,7 @@ async fn main() {
wallet_core wallet_core
.send_privacy_preserving_tx( .send_privacy_preserving_tx(
accounts, accounts,
&Program::serialize_instruction(greeting).unwrap(), Program::serialize_instruction(greeting).unwrap(),
&program.into(), &program.into(),
) )
.await .await

View File

@ -58,7 +58,7 @@ async fn main() {
wallet_core wallet_core
.send_privacy_preserving_tx( .send_privacy_preserving_tx(
accounts, accounts,
&Program::serialize_instruction(instruction).unwrap(), Program::serialize_instruction(instruction).unwrap(),
&program_with_dependencies, &program_with_dependencies,
) )
.await .await

View File

@ -101,7 +101,7 @@ async fn main() {
wallet_core wallet_core
.send_privacy_preserving_tx( .send_privacy_preserving_tx(
accounts, accounts,
&Program::serialize_instruction(instruction).unwrap(), Program::serialize_instruction(instruction).unwrap(),
&program.into(), &program.into(),
) )
.await .await
@ -142,7 +142,7 @@ async fn main() {
wallet_core wallet_core
.send_privacy_preserving_tx( .send_privacy_preserving_tx(
accounts, accounts,
&Program::serialize_instruction(instruction).unwrap(), Program::serialize_instruction(instruction).unwrap(),
&program.into(), &program.into(),
) )
.await .await

View File

@ -11,11 +11,13 @@ use tokio::test;
use wallet::cli::{ use wallet::cli::{
Command, SubcommandReturnValue, Command, SubcommandReturnValue,
account::{AccountSubcommand, NewSubcommand}, account::{AccountSubcommand, NewSubcommand},
programs::pinata::PinataProgramAgnosticSubcommand, programs::{
native_token_transfer::AuthTransferSubcommand, pinata::PinataProgramAgnosticSubcommand,
},
}; };
#[test] #[test]
async fn claim_pinata_to_public_account() -> Result<()> { async fn claim_pinata_to_existing_public_account() -> Result<()> {
let mut ctx = TestContext::new().await?; let mut ctx = TestContext::new().await?;
let pinata_prize = 150; let pinata_prize = 150;
@ -120,8 +122,26 @@ async fn claim_pinata_to_new_private_account() -> Result<()> {
anyhow::bail!("Expected RegisterAccount return value"); anyhow::bail!("Expected RegisterAccount return value");
}; };
let winner_account_id_formatted = format_private_account_id(&winner_account_id.to_string());
// Initialize account under auth transfer program
let command = Command::AuthTransfer(AuthTransferSubcommand::Init {
account_id: winner_account_id_formatted.clone(),
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let new_commitment = ctx
.wallet()
.get_private_account_commitment(&winner_account_id)
.context("Failed to get private account commitment")?;
assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await);
// Claim pinata to the new private account
let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim { let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim {
to: format_private_account_id(&winner_account_id.to_string()), to: winner_account_id_formatted,
}); });
let pinata_balance_pre = ctx let pinata_balance_pre = ctx

View File

@ -234,16 +234,16 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
]], ]],
); );
let (output, proof) = circuit::execute_and_prove( let (output, proof) = circuit::execute_and_prove(
&[sender_pre, recipient_pre], vec![sender_pre, recipient_pre],
&Program::serialize_instruction(balance_to_move).unwrap(), Program::serialize_instruction(balance_to_move).unwrap(),
&[1, 2], vec![1, 2],
&[0xdeadbeef1, 0xdeadbeef2], vec![0xdeadbeef1, 0xdeadbeef2],
&[ vec![
(sender_npk.clone(), sender_ss), (sender_npk.clone(), sender_ss),
(recipient_npk.clone(), recipient_ss), (recipient_npk.clone(), recipient_ss),
], ],
&[sender_nsk], vec![sender_nsk],
&[Some(proof)], vec![Some(proof)],
&program.into(), &program.into(),
) )
.unwrap(); .unwrap();

View File

@ -24,8 +24,9 @@ risc0-binfmt = "3.0.2"
[dev-dependencies] [dev-dependencies]
test_program_methods.workspace = true test_program_methods.workspace = true
hex-literal = "1.0.0"
env_logger.workspace = true env_logger.workspace = true
hex-literal = "1.0.0"
test-case = "3.3.1"
[features] [features]
default = [] default = []

View File

@ -15,9 +15,8 @@ pub type Nonce = u128;
/// Account to be used both in public and private contexts /// Account to be used both in public and private contexts
#[derive( #[derive(
Clone, Default, Eq, PartialEq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize, BorshSerialize, BorshDeserialize,
)] )]
#[cfg_attr(any(feature = "host", test), derive(Debug))]
pub struct Account { pub struct Account {
pub program_owner: ProgramId, pub program_owner: ProgramId,
pub balance: u128, pub balance: u128,
@ -25,8 +24,7 @@ pub struct Account {
pub nonce: Nonce, pub nonce: Nonce,
} }
#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[cfg_attr(any(feature = "host", test), derive(Debug))]
pub struct AccountWithMetadata { pub struct AccountWithMetadata {
pub account: Account, pub account: Account,
pub is_authorized: bool, pub is_authorized: bool,
@ -45,6 +43,7 @@ impl AccountWithMetadata {
} }
#[derive( #[derive(
Debug,
Default, Default,
Copy, Copy,
Clone, Clone,
@ -56,7 +55,7 @@ impl AccountWithMetadata {
BorshSerialize, BorshSerialize,
BorshDeserialize, BorshDeserialize,
)] )]
#[cfg_attr(any(feature = "host", test), derive(Debug, PartialOrd, Ord))] #[cfg_attr(any(feature = "host", test), derive(PartialOrd, Ord))]
pub struct AccountId { pub struct AccountId {
value: [u8; 32], value: [u8; 32],
} }

View File

@ -5,8 +5,7 @@ use serde::{Deserialize, Serialize};
pub const DATA_MAX_LENGTH_IN_BYTES: usize = 100 * 1024; // 100 KiB pub const DATA_MAX_LENGTH_IN_BYTES: usize = 100 * 1024; // 100 KiB
#[derive(Default, Clone, PartialEq, Eq, Serialize, BorshSerialize)] #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, BorshSerialize)]
#[cfg_attr(any(feature = "host", test), derive(Debug))]
pub struct Data(Vec<u8>); pub struct Data(Vec<u8>);
impl Data { impl Data {

View File

@ -4,8 +4,8 @@ use serde::{Deserialize, Serialize};
use crate::{Commitment, account::AccountId}; use crate::{Commitment, account::AccountId};
#[derive(Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(any(feature = "host", test), derive(Debug, Clone, Hash))] #[cfg_attr(any(feature = "host", test), derive(Clone, Hash))]
pub struct NullifierPublicKey(pub [u8; 32]); pub struct NullifierPublicKey(pub [u8; 32]);
impl From<&NullifierPublicKey> for AccountId { impl From<&NullifierPublicKey> for AccountId {

View File

@ -30,6 +30,20 @@ impl PdaSeed {
} }
} }
pub fn compute_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()
}
impl From<(&ProgramId, &PdaSeed)> for AccountId { impl From<(&ProgramId, &PdaSeed)> for AccountId {
fn from(value: (&ProgramId, &PdaSeed)) -> Self { fn from(value: (&ProgramId, &PdaSeed)) -> Self {
use risc0_zkvm::sha::{Impl, Sha256}; use risc0_zkvm::sha::{Impl, Sha256};
@ -93,6 +107,13 @@ impl AccountPostState {
} }
} }
/// Creates a post state that requests ownership of the account
/// if the account's program owner is the default program ID.
pub fn new_claimed_if_default(account: Account) -> Self {
let claim = account.program_owner == DEFAULT_PROGRAM_ID;
Self { account, claim }
}
/// Returns `true` if this post state requests that the account /// Returns `true` if this post state requests that the account
/// be claimed (owned) by the executing program. /// be claimed (owned) by the executing program.
pub fn requires_claim(&self) -> bool { pub fn requires_claim(&self) -> bool {
@ -108,6 +129,11 @@ impl AccountPostState {
pub fn account_mut(&mut self) -> &mut Account { pub fn account_mut(&mut self) -> &mut Account {
&mut self.account &mut self.account
} }
/// Consumes the post state and returns the underlying account
pub fn into_account(self) -> Account {
self.account
}
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]

View File

@ -1,11 +1,11 @@
use std::collections::HashMap; use std::collections::{HashMap, VecDeque};
use borsh::{BorshDeserialize, BorshSerialize}; use borsh::{BorshDeserialize, BorshSerialize};
use nssa_core::{ use nssa_core::{
MembershipProof, NullifierPublicKey, NullifierSecretKey, PrivacyPreservingCircuitInput, MembershipProof, NullifierPublicKey, NullifierSecretKey, PrivacyPreservingCircuitInput,
PrivacyPreservingCircuitOutput, SharedSecretKey, PrivacyPreservingCircuitOutput, SharedSecretKey,
account::AccountWithMetadata, account::AccountWithMetadata,
program::{InstructionData, ProgramId, ProgramOutput}, program::{ChainedCall, InstructionData, ProgramId, ProgramOutput},
}; };
use risc0_zkvm::{ExecutorEnv, InnerReceipt, Receipt, default_prover}; use risc0_zkvm::{ExecutorEnv, InnerReceipt, Receipt, default_prover};
@ -43,27 +43,44 @@ impl From<Program> for ProgramWithDependencies {
} }
/// Generates a proof of the execution of a NSSA program inside the privacy preserving execution /// Generates a proof of the execution of a NSSA program inside the privacy preserving execution
/// circuit /// circuit.
#[expect(clippy::too_many_arguments, reason = "TODO: fix later")] #[expect(clippy::too_many_arguments, reason = "TODO: fix later")]
pub fn execute_and_prove( pub fn execute_and_prove(
pre_states: &[AccountWithMetadata], pre_states: Vec<AccountWithMetadata>,
instruction_data: &InstructionData, instruction_data: InstructionData,
visibility_mask: &[u8], visibility_mask: Vec<u8>,
private_account_nonces: &[u128], private_account_nonces: Vec<u128>,
private_account_keys: &[(NullifierPublicKey, SharedSecretKey)], private_account_keys: Vec<(NullifierPublicKey, SharedSecretKey)>,
private_account_nsks: &[NullifierSecretKey], private_account_nsks: Vec<NullifierSecretKey>,
private_account_membership_proofs: &[Option<MembershipProof>], private_account_membership_proofs: Vec<Option<MembershipProof>>,
program_with_dependencies: &ProgramWithDependencies, program_with_dependencies: &ProgramWithDependencies,
) -> Result<(PrivacyPreservingCircuitOutput, Proof), NssaError> { ) -> Result<(PrivacyPreservingCircuitOutput, Proof), NssaError> {
let mut program = &program_with_dependencies.program; let ProgramWithDependencies {
let dependencies = &program_with_dependencies.dependencies; program,
let mut instruction_data = instruction_data.clone(); dependencies,
let mut pre_states = pre_states.to_vec(); } = program_with_dependencies;
let mut env_builder = ExecutorEnv::builder(); let mut env_builder = ExecutorEnv::builder();
let mut program_outputs = Vec::new(); let mut program_outputs = Vec::new();
for _i in 0..MAX_NUMBER_CHAINED_CALLS { let initial_call = ChainedCall {
let inner_receipt = execute_and_prove_program(program, &pre_states, &instruction_data)?; program_id: program.id(),
instruction_data: instruction_data.clone(),
pre_states,
pda_seeds: vec![],
};
let mut chained_calls = VecDeque::from_iter([(initial_call, program)]);
let mut chain_calls_counter = 0;
while let Some((chained_call, program)) = chained_calls.pop_front() {
if chain_calls_counter >= MAX_NUMBER_CHAINED_CALLS {
return Err(NssaError::MaxChainedCallsDepthExceeded);
}
let inner_receipt = execute_and_prove_program(
program,
&chained_call.pre_states,
&chained_call.instruction_data,
)?;
let program_output: ProgramOutput = inner_receipt let program_output: ProgramOutput = inner_receipt
.journal .journal
@ -76,39 +93,23 @@ pub fn execute_and_prove(
// Prove circuit. // Prove circuit.
env_builder.add_assumption(inner_receipt); env_builder.add_assumption(inner_receipt);
// TODO: Remove when multi-chain calls are supported in the circuit for new_call in program_output.chained_calls.into_iter().rev() {
assert!(program_output.chained_calls.len() <= 1); let next_program = dependencies
// TODO: Modify when multi-chain calls are supported in the circuit .get(&new_call.program_id)
if let Some(next_call) = program_output.chained_calls.first() {
program = dependencies
.get(&next_call.program_id)
.ok_or(NssaError::InvalidProgramBehavior)?; .ok_or(NssaError::InvalidProgramBehavior)?;
instruction_data = next_call.instruction_data.clone(); chained_calls.push_front((new_call, next_program));
// Build post states with metadata for next call
let mut post_states_with_metadata = Vec::new();
for (pre, post) in program_output
.pre_states
.iter()
.zip(program_output.post_states)
{
let mut post_with_metadata = pre.clone();
post_with_metadata.account = post.account().clone();
post_states_with_metadata.push(post_with_metadata);
}
pre_states = next_call.pre_states.clone();
} else {
break;
} }
chain_calls_counter += 1;
} }
let circuit_input = PrivacyPreservingCircuitInput { let circuit_input = PrivacyPreservingCircuitInput {
program_outputs, program_outputs,
visibility_mask: visibility_mask.to_vec(), visibility_mask,
private_account_nonces: private_account_nonces.to_vec(), private_account_nonces,
private_account_keys: private_account_keys.to_vec(), private_account_keys,
private_account_nsks: private_account_nsks.to_vec(), private_account_nsks,
private_account_membership_proofs: private_account_membership_proofs.to_vec(), private_account_membership_proofs,
program_id: program_with_dependencies.program.id(), program_id: program_with_dependencies.program.id(),
}; };
@ -215,13 +216,13 @@ mod tests {
let shared_secret = SharedSecretKey::new(&esk, &recipient_keys.ivk()); let shared_secret = SharedSecretKey::new(&esk, &recipient_keys.ivk());
let (output, proof) = execute_and_prove( let (output, proof) = execute_and_prove(
&[sender, recipient], vec![sender, recipient],
&Program::serialize_instruction(balance_to_move).unwrap(), Program::serialize_instruction(balance_to_move).unwrap(),
&[0, 2], vec![0, 2],
&[0xdeadbeef], vec![0xdeadbeef],
&[(recipient_keys.npk(), shared_secret)], vec![(recipient_keys.npk(), shared_secret)],
&[], vec![],
&[None], vec![None],
&Program::authenticated_transfer_program().into(), &Program::authenticated_transfer_program().into(),
) )
.unwrap(); .unwrap();
@ -311,16 +312,16 @@ mod tests {
let shared_secret_2 = SharedSecretKey::new(&esk_2, &recipient_keys.ivk()); let shared_secret_2 = SharedSecretKey::new(&esk_2, &recipient_keys.ivk());
let (output, proof) = execute_and_prove( let (output, proof) = execute_and_prove(
&[sender_pre.clone(), recipient], vec![sender_pre.clone(), recipient],
&Program::serialize_instruction(balance_to_move).unwrap(), Program::serialize_instruction(balance_to_move).unwrap(),
&[1, 2], vec![1, 2],
&[0xdeadbeef1, 0xdeadbeef2], vec![0xdeadbeef1, 0xdeadbeef2],
&[ vec![
(sender_keys.npk(), shared_secret_1), (sender_keys.npk(), shared_secret_1),
(recipient_keys.npk(), shared_secret_2), (recipient_keys.npk(), shared_secret_2),
], ],
&[sender_keys.nsk], vec![sender_keys.nsk],
&[commitment_set.get_proof_for(&commitment_sender), None], vec![commitment_set.get_proof_for(&commitment_sender), None],
&program.into(), &program.into(),
) )
.unwrap(); .unwrap();

View File

@ -226,6 +226,15 @@ mod tests {
} }
} }
pub fn changer_claimer() -> Self {
use test_program_methods::{CHANGER_CLAIMER_ELF, CHANGER_CLAIMER_ID};
Program {
id: CHANGER_CLAIMER_ID,
elf: CHANGER_CLAIMER_ELF.to_vec(),
}
}
pub fn noop() -> Self { pub fn noop() -> Self {
use test_program_methods::{NOOP_ELF, NOOP_ID}; use test_program_methods::{NOOP_ELF, NOOP_ID};
@ -235,6 +244,17 @@ mod tests {
} }
} }
pub fn malicious_authorization_changer() -> Self {
use test_program_methods::{
MALICIOUS_AUTHORIZATION_CHANGER_ELF, MALICIOUS_AUTHORIZATION_CHANGER_ID,
};
Program {
id: MALICIOUS_AUTHORIZATION_CHANGER_ID,
elf: MALICIOUS_AUTHORIZATION_CHANGER_ELF.to_vec(),
}
}
pub fn modified_transfer_program() -> Self { pub fn modified_transfer_program() -> Self {
use test_program_methods::MODIFIED_TRANSFER_ELF; use test_program_methods::MODIFIED_TRANSFER_ELF;
// This unwrap won't panic since the `MODIFIED_TRANSFER_ELF` comes from risc0 build of // This unwrap won't panic since the `MODIFIED_TRANSFER_ELF` comes from risc0 build of

View File

@ -4,7 +4,7 @@ use borsh::{BorshDeserialize, BorshSerialize};
use log::debug; use log::debug;
use nssa_core::{ use nssa_core::{
account::{Account, AccountId, AccountWithMetadata}, account::{Account, AccountId, AccountWithMetadata},
program::{ChainedCall, DEFAULT_PROGRAM_ID, PdaSeed, ProgramId, validate_execution}, program::{ChainedCall, DEFAULT_PROGRAM_ID, validate_execution},
}; };
use sha2::{Digest, digest::FixedOutput}; use sha2::{Digest, digest::FixedOutput};
@ -119,7 +119,7 @@ impl PublicTransaction {
return Err(NssaError::MaxChainedCallsDepthExceeded); return Err(NssaError::MaxChainedCallsDepthExceeded);
} }
// Check the `program_id` corresponds to a deployed program // Check that the `program_id` corresponds to a deployed program
let Some(program) = state.programs().get(&chained_call.program_id) else { let Some(program) = state.programs().get(&chained_call.program_id) else {
return Err(NssaError::InvalidInput("Unknown program".into())); return Err(NssaError::InvalidInput("Unknown program".into()));
}; };
@ -135,12 +135,14 @@ impl PublicTransaction {
chained_call.program_id, program_output chained_call.program_id, program_output
); );
let authorized_pdas = let authorized_pdas = nssa_core::program::compute_authorized_pdas(
self.compute_authorized_pdas(&caller_program_id, &chained_call.pda_seeds); caller_program_id,
&chained_call.pda_seeds,
);
for pre in &program_output.pre_states { for pre in &program_output.pre_states {
let account_id = pre.account_id; let account_id = pre.account_id;
// Check that the program output pre_states coinicide with the values in the public // Check that the program output pre_states coincide with the values in the public
// state or with any modifications to those values during the chain of calls. // state or with any modifications to those values during the chain of calls.
let expected_pre = state_diff let expected_pre = state_diff
.get(&account_id) .get(&account_id)
@ -198,22 +200,23 @@ impl PublicTransaction {
chain_calls_counter += 1; chain_calls_counter += 1;
} }
Ok(state_diff) // Check that all modified uninitialized accounts where claimed
} for post in state_diff.iter().filter_map(|(account_id, post)| {
let pre = state.get_account_by_id(account_id);
fn compute_authorized_pdas( if pre.program_owner != DEFAULT_PROGRAM_ID {
&self, return None;
caller_program_id: &Option<ProgramId>, }
pda_seeds: &[PdaSeed], if pre == *post {
) -> HashSet<AccountId> { return None;
if let Some(caller_program_id) = caller_program_id { }
pda_seeds Some(post)
.iter() }) {
.map(|pda_seed| AccountId::from((caller_program_id, pda_seed))) if post.program_owner == DEFAULT_PROGRAM_ID {
.collect() return Err(NssaError::InvalidProgramBehavior);
} else { }
HashSet::new()
} }
Ok(state_diff)
} }
} }

View File

@ -504,6 +504,7 @@ pub mod tests {
self.insert_program(Program::chain_caller()); self.insert_program(Program::chain_caller());
self.insert_program(Program::amm()); self.insert_program(Program::amm());
self.insert_program(Program::claimer()); self.insert_program(Program::claimer());
self.insert_program(Program::changer_claimer());
self self
} }
@ -865,13 +866,13 @@ pub mod tests {
let epk = EphemeralPublicKey::from_scalar(esk); let epk = EphemeralPublicKey::from_scalar(esk);
let (output, proof) = circuit::execute_and_prove( let (output, proof) = circuit::execute_and_prove(
&[sender, recipient], vec![sender, recipient],
&Program::serialize_instruction(balance_to_move).unwrap(), Program::serialize_instruction(balance_to_move).unwrap(),
&[0, 2], vec![0, 2],
&[0xdeadbeef], vec![0xdeadbeef],
&[(recipient_keys.npk(), shared_secret)], vec![(recipient_keys.npk(), shared_secret)],
&[], vec![],
&[None], vec![None],
&Program::authenticated_transfer_program().into(), &Program::authenticated_transfer_program().into(),
) )
.unwrap(); .unwrap();
@ -912,16 +913,16 @@ pub mod tests {
let epk_2 = EphemeralPublicKey::from_scalar(esk_2); let epk_2 = EphemeralPublicKey::from_scalar(esk_2);
let (output, proof) = circuit::execute_and_prove( let (output, proof) = circuit::execute_and_prove(
&[sender_pre, recipient_pre], vec![sender_pre, recipient_pre],
&Program::serialize_instruction(balance_to_move).unwrap(), Program::serialize_instruction(balance_to_move).unwrap(),
&[1, 2], vec![1, 2],
&new_nonces, new_nonces.to_vec(),
&[ vec![
(sender_keys.npk(), shared_secret_1), (sender_keys.npk(), shared_secret_1),
(recipient_keys.npk(), shared_secret_2), (recipient_keys.npk(), shared_secret_2),
], ],
&[sender_keys.nsk], vec![sender_keys.nsk],
&[state.get_proof_for_commitment(&sender_commitment), None], vec![state.get_proof_for_commitment(&sender_commitment), None],
&program.into(), &program.into(),
) )
.unwrap(); .unwrap();
@ -965,13 +966,13 @@ pub mod tests {
let epk = EphemeralPublicKey::from_scalar(esk); let epk = EphemeralPublicKey::from_scalar(esk);
let (output, proof) = circuit::execute_and_prove( let (output, proof) = circuit::execute_and_prove(
&[sender_pre, recipient_pre], vec![sender_pre, recipient_pre],
&Program::serialize_instruction(balance_to_move).unwrap(), Program::serialize_instruction(balance_to_move).unwrap(),
&[1, 0], vec![1, 0],
&[new_nonce], vec![new_nonce],
&[(sender_keys.npk(), shared_secret)], vec![(sender_keys.npk(), shared_secret)],
&[sender_keys.nsk], vec![sender_keys.nsk],
&[state.get_proof_for_commitment(&sender_commitment)], vec![state.get_proof_for_commitment(&sender_commitment)],
&program.into(), &program.into(),
) )
.unwrap(); .unwrap();
@ -1179,13 +1180,13 @@ pub mod tests {
); );
let result = execute_and_prove( let result = execute_and_prove(
&[public_account], vec![public_account],
&Program::serialize_instruction(10u128).unwrap(), Program::serialize_instruction(10u128).unwrap(),
&[0], vec![0],
&[], vec![],
&[], vec![],
&[], vec![],
&[], vec![],
&program.into(), &program.into(),
); );
@ -1206,13 +1207,13 @@ pub mod tests {
); );
let result = execute_and_prove( let result = execute_and_prove(
&[public_account], vec![public_account],
&Program::serialize_instruction(10u128).unwrap(), Program::serialize_instruction(10u128).unwrap(),
&[0], vec![0],
&[], vec![],
&[], vec![],
&[], vec![],
&[], vec![],
&program.into(), &program.into(),
); );
@ -1233,13 +1234,13 @@ pub mod tests {
); );
let result = execute_and_prove( let result = execute_and_prove(
&[public_account], vec![public_account],
&Program::serialize_instruction(()).unwrap(), Program::serialize_instruction(()).unwrap(),
&[0], vec![0],
&[], vec![],
&[], vec![],
&[], vec![],
&[], vec![],
&program.into(), &program.into(),
); );
@ -1260,13 +1261,13 @@ pub mod tests {
); );
let result = execute_and_prove( let result = execute_and_prove(
&[public_account], vec![public_account],
&Program::serialize_instruction(vec![0]).unwrap(), Program::serialize_instruction(vec![0]).unwrap(),
&[0], vec![0],
&[], vec![],
&[], vec![],
&[], vec![],
&[], vec![],
&program.into(), &program.into(),
); );
@ -1289,13 +1290,13 @@ pub mod tests {
let large_data: Vec<u8> = vec![0; nssa_core::account::data::DATA_MAX_LENGTH_IN_BYTES + 1]; let large_data: Vec<u8> = vec![0; nssa_core::account::data::DATA_MAX_LENGTH_IN_BYTES + 1];
let result = execute_and_prove( let result = execute_and_prove(
&[public_account], vec![public_account],
&Program::serialize_instruction(large_data).unwrap(), Program::serialize_instruction(large_data).unwrap(),
&[0], vec![0],
&[], vec![],
&[], vec![],
&[], vec![],
&[], vec![],
&program.to_owned().into(), &program.to_owned().into(),
); );
@ -1316,13 +1317,13 @@ pub mod tests {
); );
let result = execute_and_prove( let result = execute_and_prove(
&[public_account], vec![public_account],
&Program::serialize_instruction(()).unwrap(), Program::serialize_instruction(()).unwrap(),
&[0], vec![0],
&[], vec![],
&[], vec![],
&[], vec![],
&[], vec![],
&program.into(), &program.into(),
); );
@ -1352,13 +1353,13 @@ pub mod tests {
); );
let result = execute_and_prove( let result = execute_and_prove(
&[public_account_1, public_account_2], vec![public_account_1, public_account_2],
&Program::serialize_instruction(()).unwrap(), Program::serialize_instruction(()).unwrap(),
&[0, 0], vec![0, 0],
&[], vec![],
&[], vec![],
&[], vec![],
&[], vec![],
&program.into(), &program.into(),
); );
@ -1379,13 +1380,13 @@ pub mod tests {
); );
let result = execute_and_prove( let result = execute_and_prove(
&[public_account], vec![public_account],
&Program::serialize_instruction(()).unwrap(), Program::serialize_instruction(()).unwrap(),
&[0], vec![0],
&[], vec![],
&[], vec![],
&[], vec![],
&[], vec![],
&program.into(), &program.into(),
); );
@ -1415,13 +1416,13 @@ pub mod tests {
); );
let result = execute_and_prove( let result = execute_and_prove(
&[public_account_1, public_account_2], vec![public_account_1, public_account_2],
&Program::serialize_instruction(10u128).unwrap(), Program::serialize_instruction(10u128).unwrap(),
&[0, 0], vec![0, 0],
&[], vec![],
&[], vec![],
&[], vec![],
&[], vec![],
&program.into(), &program.into(),
); );
@ -1453,13 +1454,13 @@ pub mod tests {
// Setting only one visibility mask for a circuit execution with two pre_state accounts. // Setting only one visibility mask for a circuit execution with two pre_state accounts.
let visibility_mask = [0]; let visibility_mask = [0];
let result = execute_and_prove( let result = execute_and_prove(
&[public_account_1, public_account_2], vec![public_account_1, public_account_2],
&Program::serialize_instruction(10u128).unwrap(), Program::serialize_instruction(10u128).unwrap(),
&visibility_mask, visibility_mask.to_vec(),
&[], vec![],
&[], vec![],
&[], vec![],
&[], vec![],
&program.into(), &program.into(),
); );
@ -1486,11 +1487,11 @@ pub mod tests {
// Setting only one nonce for an execution with two private accounts. // Setting only one nonce for an execution with two private accounts.
let private_account_nonces = [0xdeadbeef1]; let private_account_nonces = [0xdeadbeef1];
let result = execute_and_prove( let result = execute_and_prove(
&[private_account_1, private_account_2], vec![private_account_1, private_account_2],
&Program::serialize_instruction(10u128).unwrap(), Program::serialize_instruction(10u128).unwrap(),
&[1, 2], vec![1, 2],
&private_account_nonces, private_account_nonces.to_vec(),
&[ vec![
( (
sender_keys.npk(), sender_keys.npk(),
SharedSecretKey::new(&[55; 32], &sender_keys.ivk()), SharedSecretKey::new(&[55; 32], &sender_keys.ivk()),
@ -1500,8 +1501,8 @@ pub mod tests {
SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()),
), ),
], ],
&[sender_keys.nsk], vec![sender_keys.nsk],
&[Some((0, vec![]))], vec![Some((0, vec![]))],
&program.into(), &program.into(),
); );
@ -1530,13 +1531,13 @@ pub mod tests {
SharedSecretKey::new(&[55; 32], &sender_keys.ivk()), SharedSecretKey::new(&[55; 32], &sender_keys.ivk()),
)]; )];
let result = execute_and_prove( let result = execute_and_prove(
&[private_account_1, private_account_2], vec![private_account_1, private_account_2],
&Program::serialize_instruction(10u128).unwrap(), Program::serialize_instruction(10u128).unwrap(),
&[1, 2], vec![1, 2],
&[0xdeadbeef1, 0xdeadbeef2], vec![0xdeadbeef1, 0xdeadbeef2],
&private_account_keys, private_account_keys.to_vec(),
&[sender_keys.nsk], vec![sender_keys.nsk],
&[Some((0, vec![]))], vec![Some((0, vec![]))],
&program.into(), &program.into(),
); );
@ -1563,11 +1564,11 @@ pub mod tests {
// Setting no second commitment proof. // Setting no second commitment proof.
let private_account_membership_proofs = [Some((0, vec![]))]; let private_account_membership_proofs = [Some((0, vec![]))];
let result = execute_and_prove( let result = execute_and_prove(
&[private_account_1, private_account_2], vec![private_account_1, private_account_2],
&Program::serialize_instruction(10u128).unwrap(), Program::serialize_instruction(10u128).unwrap(),
&[1, 2], vec![1, 2],
&[0xdeadbeef1, 0xdeadbeef2], vec![0xdeadbeef1, 0xdeadbeef2],
&[ vec![
( (
sender_keys.npk(), sender_keys.npk(),
SharedSecretKey::new(&[55; 32], &sender_keys.ivk()), SharedSecretKey::new(&[55; 32], &sender_keys.ivk()),
@ -1577,8 +1578,8 @@ pub mod tests {
SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()),
), ),
], ],
&[sender_keys.nsk], vec![sender_keys.nsk],
&private_account_membership_proofs, private_account_membership_proofs.to_vec(),
&program.into(), &program.into(),
); );
@ -1605,11 +1606,11 @@ pub mod tests {
// Setting no auth key for an execution with one non default private accounts. // Setting no auth key for an execution with one non default private accounts.
let private_account_nsks = []; let private_account_nsks = [];
let result = execute_and_prove( let result = execute_and_prove(
&[private_account_1, private_account_2], vec![private_account_1, private_account_2],
&Program::serialize_instruction(10u128).unwrap(), Program::serialize_instruction(10u128).unwrap(),
&[1, 2], vec![1, 2],
&[0xdeadbeef1, 0xdeadbeef2], vec![0xdeadbeef1, 0xdeadbeef2],
&[ vec![
( (
sender_keys.npk(), sender_keys.npk(),
SharedSecretKey::new(&[55; 32], &sender_keys.ivk()), SharedSecretKey::new(&[55; 32], &sender_keys.ivk()),
@ -1619,8 +1620,8 @@ pub mod tests {
SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()),
), ),
], ],
&private_account_nsks, private_account_nsks.to_vec(),
&[], vec![],
&program.into(), &program.into(),
); );
@ -1663,13 +1664,13 @@ pub mod tests {
let private_account_nsks = [recipient_keys.nsk]; let private_account_nsks = [recipient_keys.nsk];
let private_account_membership_proofs = [Some((0, vec![]))]; let private_account_membership_proofs = [Some((0, vec![]))];
let result = execute_and_prove( let result = execute_and_prove(
&[private_account_1, private_account_2], vec![private_account_1, private_account_2],
&Program::serialize_instruction(10u128).unwrap(), Program::serialize_instruction(10u128).unwrap(),
&[1, 2], vec![1, 2],
&[0xdeadbeef1, 0xdeadbeef2], vec![0xdeadbeef1, 0xdeadbeef2],
&private_account_keys, private_account_keys.to_vec(),
&private_account_nsks, private_account_nsks.to_vec(),
&private_account_membership_proofs, private_account_membership_proofs.to_vec(),
&program.into(), &program.into(),
); );
@ -1701,11 +1702,11 @@ pub mod tests {
); );
let result = execute_and_prove( let result = execute_and_prove(
&[private_account_1, private_account_2], vec![private_account_1, private_account_2],
&Program::serialize_instruction(10u128).unwrap(), Program::serialize_instruction(10u128).unwrap(),
&[1, 2], vec![1, 2],
&[0xdeadbeef1, 0xdeadbeef2], vec![0xdeadbeef1, 0xdeadbeef2],
&[ vec![
( (
sender_keys.npk(), sender_keys.npk(),
SharedSecretKey::new(&[55; 32], &sender_keys.ivk()), SharedSecretKey::new(&[55; 32], &sender_keys.ivk()),
@ -1715,8 +1716,8 @@ pub mod tests {
SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()),
), ),
], ],
&[sender_keys.nsk], vec![sender_keys.nsk],
&[Some((0, vec![]))], vec![Some((0, vec![]))],
&program.into(), &program.into(),
); );
@ -1749,11 +1750,11 @@ pub mod tests {
); );
let result = execute_and_prove( let result = execute_and_prove(
&[private_account_1, private_account_2], vec![private_account_1, private_account_2],
&Program::serialize_instruction(10u128).unwrap(), Program::serialize_instruction(10u128).unwrap(),
&[1, 2], vec![1, 2],
&[0xdeadbeef1, 0xdeadbeef2], vec![0xdeadbeef1, 0xdeadbeef2],
&[ vec![
( (
sender_keys.npk(), sender_keys.npk(),
SharedSecretKey::new(&[55; 32], &sender_keys.ivk()), SharedSecretKey::new(&[55; 32], &sender_keys.ivk()),
@ -1763,8 +1764,8 @@ pub mod tests {
SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()),
), ),
], ],
&[sender_keys.nsk], vec![sender_keys.nsk],
&[Some((0, vec![]))], vec![Some((0, vec![]))],
&program.into(), &program.into(),
); );
@ -1796,11 +1797,11 @@ pub mod tests {
); );
let result = execute_and_prove( let result = execute_and_prove(
&[private_account_1, private_account_2], vec![private_account_1, private_account_2],
&Program::serialize_instruction(10u128).unwrap(), Program::serialize_instruction(10u128).unwrap(),
&[1, 2], vec![1, 2],
&[0xdeadbeef1, 0xdeadbeef2], vec![0xdeadbeef1, 0xdeadbeef2],
&[ vec![
( (
sender_keys.npk(), sender_keys.npk(),
SharedSecretKey::new(&[55; 32], &sender_keys.ivk()), SharedSecretKey::new(&[55; 32], &sender_keys.ivk()),
@ -1810,8 +1811,8 @@ pub mod tests {
SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()),
), ),
], ],
&[sender_keys.nsk], vec![sender_keys.nsk],
&[Some((0, vec![]))], vec![Some((0, vec![]))],
&program.into(), &program.into(),
); );
@ -1843,11 +1844,11 @@ pub mod tests {
); );
let result = execute_and_prove( let result = execute_and_prove(
&[private_account_1, private_account_2], vec![private_account_1, private_account_2],
&Program::serialize_instruction(10u128).unwrap(), Program::serialize_instruction(10u128).unwrap(),
&[1, 2], vec![1, 2],
&[0xdeadbeef1, 0xdeadbeef2], vec![0xdeadbeef1, 0xdeadbeef2],
&[ vec![
( (
sender_keys.npk(), sender_keys.npk(),
SharedSecretKey::new(&[55; 32], &sender_keys.ivk()), SharedSecretKey::new(&[55; 32], &sender_keys.ivk()),
@ -1857,8 +1858,8 @@ pub mod tests {
SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()),
), ),
], ],
&[sender_keys.nsk], vec![sender_keys.nsk],
&[Some((0, vec![]))], vec![Some((0, vec![]))],
&program.into(), &program.into(),
); );
@ -1888,11 +1889,11 @@ pub mod tests {
); );
let result = execute_and_prove( let result = execute_and_prove(
&[private_account_1, private_account_2], vec![private_account_1, private_account_2],
&Program::serialize_instruction(10u128).unwrap(), Program::serialize_instruction(10u128).unwrap(),
&[1, 2], vec![1, 2],
&[0xdeadbeef1, 0xdeadbeef2], vec![0xdeadbeef1, 0xdeadbeef2],
&[ vec![
( (
sender_keys.npk(), sender_keys.npk(),
SharedSecretKey::new(&[55; 32], &sender_keys.ivk()), SharedSecretKey::new(&[55; 32], &sender_keys.ivk()),
@ -1902,8 +1903,8 @@ pub mod tests {
SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()),
), ),
], ],
&[sender_keys.nsk], vec![sender_keys.nsk],
&[Some((0, vec![]))], vec![Some((0, vec![]))],
&program.into(), &program.into(),
); );
@ -1927,13 +1928,13 @@ pub mod tests {
let visibility_mask = [0, 3]; let visibility_mask = [0, 3];
let result = execute_and_prove( let result = execute_and_prove(
&[public_account_1, public_account_2], vec![public_account_1, public_account_2],
&Program::serialize_instruction(10u128).unwrap(), Program::serialize_instruction(10u128).unwrap(),
&visibility_mask, visibility_mask.to_vec(),
&[], vec![],
&[], vec![],
&[], vec![],
&[], vec![],
&program.into(), &program.into(),
); );
@ -1961,11 +1962,11 @@ pub mod tests {
// accounts. // accounts.
let private_account_nonces = [0xdeadbeef1, 0xdeadbeef2, 0xdeadbeef3]; let private_account_nonces = [0xdeadbeef1, 0xdeadbeef2, 0xdeadbeef3];
let result = execute_and_prove( let result = execute_and_prove(
&[private_account_1, private_account_2], vec![private_account_1, private_account_2],
&Program::serialize_instruction(10u128).unwrap(), Program::serialize_instruction(10u128).unwrap(),
&[1, 2], vec![1, 2],
&private_account_nonces, private_account_nonces.to_vec(),
&[ vec![
( (
sender_keys.npk(), sender_keys.npk(),
SharedSecretKey::new(&[55; 32], &sender_keys.ivk()), SharedSecretKey::new(&[55; 32], &sender_keys.ivk()),
@ -1975,8 +1976,8 @@ pub mod tests {
SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()),
), ),
], ],
&[sender_keys.nsk], vec![sender_keys.nsk],
&[Some((0, vec![]))], vec![Some((0, vec![]))],
&program.into(), &program.into(),
); );
@ -2017,13 +2018,13 @@ pub mod tests {
), ),
]; ];
let result = execute_and_prove( let result = execute_and_prove(
&[private_account_1, private_account_2], vec![private_account_1, private_account_2],
&Program::serialize_instruction(10u128).unwrap(), Program::serialize_instruction(10u128).unwrap(),
&[1, 2], vec![1, 2],
&[0xdeadbeef1, 0xdeadbeef2], vec![0xdeadbeef1, 0xdeadbeef2],
&private_account_keys, private_account_keys.to_vec(),
&[sender_keys.nsk], vec![sender_keys.nsk],
&[Some((0, vec![]))], vec![Some((0, vec![]))],
&program.into(), &program.into(),
); );
@ -2053,11 +2054,11 @@ pub mod tests {
let private_account_nsks = [sender_keys.nsk, recipient_keys.nsk]; let private_account_nsks = [sender_keys.nsk, recipient_keys.nsk];
let private_account_membership_proofs = [Some((0, vec![])), Some((1, vec![]))]; let private_account_membership_proofs = [Some((0, vec![])), Some((1, vec![]))];
let result = execute_and_prove( let result = execute_and_prove(
&[private_account_1, private_account_2], vec![private_account_1, private_account_2],
&Program::serialize_instruction(10u128).unwrap(), Program::serialize_instruction(10u128).unwrap(),
&visibility_mask, visibility_mask.to_vec(),
&[0xdeadbeef1, 0xdeadbeef2], vec![0xdeadbeef1, 0xdeadbeef2],
&[ vec![
( (
sender_keys.npk(), sender_keys.npk(),
SharedSecretKey::new(&[55; 32], &sender_keys.ivk()), SharedSecretKey::new(&[55; 32], &sender_keys.ivk()),
@ -2067,8 +2068,8 @@ pub mod tests {
SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()),
), ),
], ],
&private_account_nsks, private_account_nsks.to_vec(),
&private_account_membership_proofs, private_account_membership_proofs.to_vec(),
&program.into(), &program.into(),
); );
@ -2149,16 +2150,16 @@ pub mod tests {
let private_account_membership_proofs = [Some((1, vec![])), Some((1, vec![]))]; let private_account_membership_proofs = [Some((1, vec![])), Some((1, vec![]))];
let shared_secret = SharedSecretKey::new(&[55; 32], &sender_keys.ivk()); let shared_secret = SharedSecretKey::new(&[55; 32], &sender_keys.ivk());
let result = execute_and_prove( let result = execute_and_prove(
&[private_account_1.clone(), private_account_1], vec![private_account_1.clone(), private_account_1],
&Program::serialize_instruction(100u128).unwrap(), Program::serialize_instruction(100u128).unwrap(),
&visibility_mask, visibility_mask.to_vec(),
&[0xdeadbeef1, 0xdeadbeef2], vec![0xdeadbeef1, 0xdeadbeef2],
&[ vec![
(sender_keys.npk(), shared_secret), (sender_keys.npk(), shared_secret),
(sender_keys.npk(), shared_secret), (sender_keys.npk(), shared_secret),
], ],
&private_account_nsks, private_account_nsks.to_vec(),
&private_account_membership_proofs, private_account_membership_proofs.to_vec(),
&program.into(), &program.into(),
); );
@ -3941,8 +3942,9 @@ pub mod tests {
assert_eq!(to_post, expected_to_post); assert_eq!(to_post, expected_to_post);
} }
#[test] #[test_case::test_case(1; "single call")]
fn test_private_chained_call() { #[test_case::test_case(2; "two calls")]
fn test_private_chained_call(number_of_calls: u32) {
// Arrange // Arrange
let chain_caller = Program::chain_caller(); let chain_caller = Program::chain_caller();
let auth_transfers = Program::authenticated_transfer_program(); let auth_transfers = Program::authenticated_transfer_program();
@ -3978,7 +3980,7 @@ pub mod tests {
let instruction: (u128, ProgramId, u32, Option<PdaSeed>) = ( let instruction: (u128, ProgramId, u32, Option<PdaSeed>) = (
amount, amount,
Program::authenticated_transfer_program().id(), Program::authenticated_transfer_program().id(),
1, number_of_calls,
None, None,
); );
@ -3999,14 +4001,14 @@ pub mod tests {
let to_new_nonce = 0xdeadbeef2; let to_new_nonce = 0xdeadbeef2;
let from_expected_post = Account { let from_expected_post = Account {
balance: initial_balance - amount, balance: initial_balance - number_of_calls as u128 * amount,
nonce: from_new_nonce, nonce: from_new_nonce,
..from_account.account.clone() ..from_account.account.clone()
}; };
let from_expected_commitment = Commitment::new(&from_keys.npk(), &from_expected_post); let from_expected_commitment = Commitment::new(&from_keys.npk(), &from_expected_post);
let to_expected_post = Account { let to_expected_post = Account {
balance: amount, balance: number_of_calls as u128 * amount,
nonce: to_new_nonce, nonce: to_new_nonce,
..to_account.account.clone() ..to_account.account.clone()
}; };
@ -4014,13 +4016,13 @@ pub mod tests {
// Act // Act
let (output, proof) = execute_and_prove( let (output, proof) = execute_and_prove(
&[to_account, from_account], vec![to_account, from_account],
&Program::serialize_instruction(instruction).unwrap(), Program::serialize_instruction(instruction).unwrap(),
&[1, 1], vec![1, 1],
&[from_new_nonce, to_new_nonce], vec![from_new_nonce, to_new_nonce],
&[(from_keys.npk(), to_ss), (to_keys.npk(), from_ss)], vec![(from_keys.npk(), to_ss), (to_keys.npk(), from_ss)],
&[from_keys.nsk, to_keys.nsk], vec![from_keys.nsk, to_keys.nsk],
&[ vec![
state.get_proof_for_commitment(&from_commitment), state.get_proof_for_commitment(&from_commitment),
state.get_proof_for_commitment(&to_commitment), state.get_proof_for_commitment(&to_commitment),
], ],
@ -4255,13 +4257,13 @@ pub mod tests {
// Execute and prove the circuit with the authorized account but no commitment proof // Execute and prove the circuit with the authorized account but no commitment proof
let (output, proof) = execute_and_prove( let (output, proof) = execute_and_prove(
std::slice::from_ref(&authorized_account), vec![authorized_account],
&Program::serialize_instruction(balance).unwrap(), Program::serialize_instruction(balance).unwrap(),
&[1], vec![1],
&[nonce], vec![nonce],
&[(private_keys.npk(), shared_secret)], vec![(private_keys.npk(), shared_secret)],
&[private_keys.nsk], vec![private_keys.nsk],
&[None], vec![None],
&program.into(), &program.into(),
) )
.unwrap(); .unwrap();
@ -4308,13 +4310,13 @@ pub mod tests {
// Step 2: Execute claimer program to claim the account with authentication // Step 2: Execute claimer program to claim the account with authentication
let (output, proof) = execute_and_prove( let (output, proof) = execute_and_prove(
std::slice::from_ref(&authorized_account), vec![authorized_account.clone()],
&Program::serialize_instruction(balance).unwrap(), Program::serialize_instruction(balance).unwrap(),
&[1], vec![1],
&[nonce], vec![nonce],
&[(private_keys.npk(), shared_secret)], vec![(private_keys.npk(), shared_secret)],
&[private_keys.nsk], vec![private_keys.nsk],
&[None], vec![None],
&claimer_program.into(), &claimer_program.into(),
) )
.unwrap(); .unwrap();
@ -4356,16 +4358,174 @@ pub mod tests {
// Step 3: Try to execute noop program with authentication but without initialization // Step 3: Try to execute noop program with authentication but without initialization
let res = execute_and_prove( let res = execute_and_prove(
std::slice::from_ref(&account_metadata), vec![account_metadata],
&Program::serialize_instruction(()).unwrap(), Program::serialize_instruction(()).unwrap(),
&[1], vec![1],
&[nonce2], vec![nonce2],
&[(private_keys.npk(), shared_secret2)], vec![(private_keys.npk(), shared_secret2)],
&[private_keys.nsk], vec![private_keys.nsk],
&[None], vec![None],
&noop_program.into(), &noop_program.into(),
); );
assert!(matches!(res, Err(NssaError::CircuitProvingError(_)))); assert!(matches!(res, Err(NssaError::CircuitProvingError(_))));
} }
#[test]
fn test_public_changer_claimer_no_data_change_no_claim_succeeds() {
let initial_data = [];
let mut state =
V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
let account_id = AccountId::new([1; 32]);
let program_id = Program::changer_claimer().id();
// Don't change data (None) and don't claim (false)
let instruction: (Option<Vec<u8>>, bool) = (None, false);
let message =
public_transaction::Message::try_new(program_id, vec![account_id], vec![], instruction)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx);
// Should succeed - no changes made, no claim needed
assert!(result.is_ok());
// Account should remain default/unclaimed
assert_eq!(state.get_account_by_id(&account_id), Account::default());
}
#[test]
fn test_public_changer_claimer_data_change_no_claim_fails() {
let initial_data = [];
let mut state =
V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
let account_id = AccountId::new([1; 32]);
let program_id = Program::changer_claimer().id();
// Change data but don't claim (false) - should fail
let new_data = vec![1, 2, 3, 4, 5];
let instruction: (Option<Vec<u8>>, bool) = (Some(new_data), false);
let message =
public_transaction::Message::try_new(program_id, vec![account_id], vec![], instruction)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx);
// Should fail - cannot modify data without claiming the account
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
}
#[test]
fn test_private_changer_claimer_no_data_change_no_claim_succeeds() {
let program = Program::changer_claimer();
let sender_keys = test_private_account_keys_1();
let private_account =
AccountWithMetadata::new(Account::default(), true, &sender_keys.npk());
// Don't change data (None) and don't claim (false)
let instruction: (Option<Vec<u8>>, bool) = (None, false);
let result = execute_and_prove(
vec![private_account],
Program::serialize_instruction(instruction).unwrap(),
vec![1],
vec![2],
vec![(
sender_keys.npk(),
SharedSecretKey::new(&[3; 32], &sender_keys.ivk()),
)],
vec![sender_keys.nsk],
vec![Some((0, vec![]))],
&program.into(),
);
// Should succeed - no changes made, no claim needed
assert!(result.is_ok());
}
#[test]
fn test_private_changer_claimer_data_change_no_claim_fails() {
let program = Program::changer_claimer();
let sender_keys = test_private_account_keys_1();
let private_account =
AccountWithMetadata::new(Account::default(), true, &sender_keys.npk());
// Change data but don't claim (false) - should fail
let new_data = vec![1, 2, 3, 4, 5];
let instruction: (Option<Vec<u8>>, bool) = (Some(new_data), false);
let result = execute_and_prove(
vec![private_account],
Program::serialize_instruction(instruction).unwrap(),
vec![1],
vec![2],
vec![(
sender_keys.npk(),
SharedSecretKey::new(&[3; 32], &sender_keys.ivk()),
)],
vec![sender_keys.nsk],
vec![Some((0, vec![]))],
&program.into(),
);
// Should fail - cannot modify data without claiming the account
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
}
#[test]
fn test_malicious_authorization_changer_should_fail_in_privacy_preserving_circuit() {
// Arrange
let malicious_program = Program::malicious_authorization_changer();
let auth_transfers = Program::authenticated_transfer_program();
let sender_keys = test_public_account_keys_1();
let recipient_keys = test_private_account_keys_1();
let sender_account = AccountWithMetadata::new(
Account {
program_owner: auth_transfers.id(),
balance: 100,
..Default::default()
},
false,
sender_keys.account_id(),
);
let recipient_account =
AccountWithMetadata::new(Account::default(), true, &recipient_keys.npk());
let recipient_commitment =
Commitment::new(&recipient_keys.npk(), &recipient_account.account);
let state = V02State::new_with_genesis_accounts(
&[(sender_account.account_id, sender_account.account.balance)],
std::slice::from_ref(&recipient_commitment),
)
.with_test_programs();
let balance_to_transfer = 10u128;
let instruction = (balance_to_transfer, auth_transfers.id());
let recipient_esk = [3; 32];
let recipient = SharedSecretKey::new(&recipient_esk, &recipient_keys.ivk());
let mut dependencies = HashMap::new();
dependencies.insert(auth_transfers.id(), auth_transfers);
let program_with_deps = ProgramWithDependencies::new(malicious_program, dependencies);
let recipient_new_nonce = 0xdeadbeef1;
// Act - execute the malicious program - this should fail during proving
let result = execute_and_prove(
vec![sender_account, recipient_account],
Program::serialize_instruction(instruction).unwrap(),
vec![0, 1],
vec![recipient_new_nonce],
vec![(recipient_keys.npk(), recipient)],
vec![recipient_keys.nsk],
vec![state.get_proof_for_commitment(&recipient_commitment)],
&program_with_deps,
);
// Assert - should fail because the malicious program tries to manipulate is_authorized
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
}
} }

View File

@ -77,7 +77,7 @@ fn main() {
instruction_words, instruction_words,
vec![pinata, winner], vec![pinata, winner],
vec![ vec![
AccountPostState::new(pinata_post), AccountPostState::new_claimed_if_default(pinata_post),
AccountPostState::new(winner_post), AccountPostState::new(winner_post),
], ],
); );

View File

@ -1,12 +1,18 @@
use std::collections::HashMap; use std::{
collections::{HashMap, HashSet, VecDeque, hash_map::Entry},
convert::Infallible,
};
use nssa_core::{ use nssa_core::{
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier, Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, MembershipProof,
NullifierPublicKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, Nullifier, NullifierPublicKey, NullifierSecretKey, PrivacyPreservingCircuitInput,
account::{Account, AccountId, AccountWithMetadata}, PrivacyPreservingCircuitOutput, SharedSecretKey,
account::{Account, AccountId, AccountWithMetadata, Nonce},
compute_digest_for_path, compute_digest_for_path,
encryption::Ciphertext, program::{
program::{DEFAULT_PROGRAM_ID, MAX_NUMBER_CHAINED_CALLS, validate_execution}, AccountPostState, ChainedCall, DEFAULT_PROGRAM_ID, MAX_NUMBER_CHAINED_CALLS, ProgramId,
ProgramOutput, validate_execution,
},
}; };
use risc0_zkvm::{guest::env, serde::to_vec}; use risc0_zkvm::{guest::env, serde::to_vec};
@ -18,118 +24,224 @@ fn main() {
private_account_keys, private_account_keys,
private_account_nsks, private_account_nsks,
private_account_membership_proofs, private_account_membership_proofs,
mut program_id, program_id,
} = env::read(); } = env::read();
let mut pre_states: Vec<AccountWithMetadata> = Vec::new(); let execution_state = ExecutionState::derive_from_outputs(program_id, program_outputs);
let mut state_diff: HashMap<AccountId, Account> = HashMap::new();
let num_calls = program_outputs.len(); let output = compute_circuit_output(
if num_calls > MAX_NUMBER_CHAINED_CALLS { execution_state,
panic!("Max chained calls depth is exceeded"); &visibility_mask,
} &private_account_nonces,
&private_account_keys,
&private_account_nsks,
&private_account_membership_proofs,
);
let Some(last_program_call) = program_outputs.last() else { env::commit(&output);
panic!("Program outputs is empty") }
};
if !last_program_call.chained_calls.is_empty() { /// State of the involved accounts before and after program execution.
panic!("Call stack is incomplete"); struct ExecutionState {
} pre_states: Vec<AccountWithMetadata>,
post_states: HashMap<AccountId, Account>,
}
for window in program_outputs.windows(2) { impl ExecutionState {
let caller = &window[0]; /// Validate program outputs and derive the overall execution state.
let callee = &window[1]; pub fn derive_from_outputs(program_id: ProgramId, program_outputs: Vec<ProgramOutput>) -> Self {
let Some(first_output) = program_outputs.first() else {
if caller.chained_calls.len() > 1 { panic!("No program outputs provided");
panic!("Privacy Multi-chained calls are not supported yet");
}
// TODO: Modify when multi-chain calls are supported in the circuit
let Some(caller_chained_call) = &caller.chained_calls.first() else {
panic!("Expected chained call");
}; };
// Check that instruction data in caller is the instruction data in callee let initial_call = ChainedCall {
if caller_chained_call.instruction_data != callee.instruction_data {
panic!("Invalid instruction data");
}
// Check that account pre_states in caller are the ones in calle
if caller_chained_call.pre_states != callee.pre_states {
panic!("Invalid pre states");
}
}
for (i, program_output) in program_outputs.iter().enumerate() {
let mut program_output = program_output.clone();
// Check that `program_output` is consistent with the execution of the corresponding
// program.
let program_output_words =
&to_vec(&program_output).expect("program_output must be serializable");
env::verify(program_id, program_output_words)
.expect("program output must match the program's execution");
// Check that the program is well behaved.
// See the # Programs section for the definition of the `validate_execution` method.
if !validate_execution(
&program_output.pre_states,
&program_output.post_states,
program_id, program_id,
) { instruction_data: first_output.instruction_data.clone(),
panic!("Bad behaved program"); pre_states: first_output.pre_states.clone(),
} pda_seeds: Vec::new(),
};
let mut chained_calls = VecDeque::from_iter([(initial_call, None)]);
// The invoked program claims the accounts with default program id. let mut execution_state = ExecutionState {
for post in program_output pre_states: Vec::new(),
.post_states post_states: HashMap::new(),
.iter_mut() };
.filter(|post| post.requires_claim())
{ let mut program_outputs_iter = program_outputs.into_iter();
// The invoked program can only claim accounts with default program id. let mut chain_calls_counter = 0;
if post.account().program_owner == DEFAULT_PROGRAM_ID {
post.account_mut().program_owner = program_id; while let Some((chained_call, caller_program_id)) = chained_calls.pop_front() {
} else { assert!(
panic!("Cannot claim an initialized account") chain_calls_counter <= MAX_NUMBER_CHAINED_CALLS,
"Max chained calls depth is exceeded"
);
let Some(program_output) = program_outputs_iter.next() else {
panic!("Insufficient program outputs for chained calls");
};
// Check that instruction data in chained call is the instruction data in program output
assert_eq!(
chained_call.instruction_data, program_output.instruction_data,
"Mismatched instruction data between chained call and program output"
);
// Check that `program_output` is consistent with the execution of the corresponding
// program.
let program_output_words =
&to_vec(&program_output).expect("program_output must be serializable");
env::verify(chained_call.program_id, program_output_words).unwrap_or_else(
|_: Infallible| unreachable!("Infallible error is never constructed"),
);
// Check that the program is well behaved.
// See the # Programs section for the definition of the `validate_execution` method.
let execution_valid = validate_execution(
&program_output.pre_states,
&program_output.post_states,
chained_call.program_id,
);
assert!(execution_valid, "Bad behaved program");
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(
chained_call.program_id,
authorized_pdas,
program_output.pre_states,
program_output.post_states,
);
chain_calls_counter += 1;
} }
for (pre, post) in program_output assert!(
program_outputs_iter.next().is_none(),
"Inner call without a chained call found",
);
// Check that all modified uninitialized accounts were claimed
for (account_id, post) in execution_state
.pre_states .pre_states
.iter() .iter()
.zip(&program_output.post_states) .filter(|a| a.account.program_owner == DEFAULT_PROGRAM_ID)
.map(|a| {
let post = execution_state
.post_states
.get(&a.account_id)
.expect("Post state must exist for pre state");
(a, post)
})
.filter(|(pre_default, post)| pre_default.account != **post)
.map(|(pre, post)| (pre.account_id, post))
{ {
if let Some(account_pre) = state_diff.get(&pre.account_id) { assert_ne!(
if account_pre != &pre.account { post.program_owner, DEFAULT_PROGRAM_ID,
panic!("Invalid input"); "Account {account_id:?} was modified but not claimed"
} );
} else {
pre_states.push(pre.clone());
}
state_diff.insert(pre.account_id, post.account().clone());
} }
// TODO: Modify when multi-chain calls are supported in the circuit execution_state
if let Some(next_chained_call) = &program_output.chained_calls.first() {
program_id = next_chained_call.program_id;
} else if i != program_outputs.len() - 1 {
panic!("Inner call without a chained call found")
};
} }
let n_accounts = pre_states.len(); /// Validate program pre and post states and populate the execution state.
if visibility_mask.len() != n_accounts { fn validate_and_sync_states(
panic!("Invalid visibility mask length"); &mut self,
program_id: ProgramId,
authorized_pdas: HashSet<AccountId>,
pre_states: Vec<AccountWithMetadata>,
post_states: Vec<AccountPostState>,
) {
for (pre, mut post) in pre_states.into_iter().zip(post_states) {
let pre_account_id = pre.account_id;
let post_states_entry = self.post_states.entry(pre.account_id);
match &post_states_entry {
Entry::Occupied(occupied) => {
// Ensure that new pre state is the same as known post state
assert_eq!(
occupied.get(),
&pre.account,
"Inconsistent pre state for account {pre_account_id:?}",
);
let previous_is_authorized = self
.pre_states
.iter()
.find(|acc| acc.account_id == pre_account_id)
.map(|acc| acc.is_authorized)
.unwrap_or_else(|| {
panic!(
"Pre state must exist in execution state for account {pre_account_id:?}",
)
});
let is_authorized =
previous_is_authorized || authorized_pdas.contains(&pre_account_id);
assert_eq!(
pre.is_authorized, is_authorized,
"Inconsistent authorization for account {pre_account_id:?}",
);
}
Entry::Vacant(_) => {
self.pre_states.push(pre);
}
}
if post.requires_claim() {
// The invoked program can only claim accounts with default program id.
if post.account().program_owner == DEFAULT_PROGRAM_ID {
post.account_mut().program_owner = program_id;
} else {
panic!("Cannot claim an initialized account {pre_account_id:?}");
}
}
post_states_entry.insert_entry(post.into_account());
}
} }
// These lists will be the public outputs of this circuit /// Get an iterator over pre and post states of each account involved in the execution.
// and will be populated next. pub fn into_states_iter(
let mut public_pre_states: Vec<AccountWithMetadata> = Vec::new(); mut self,
let mut public_post_states: Vec<Account> = Vec::new(); ) -> impl ExactSizeIterator<Item = (AccountWithMetadata, Account)> {
let mut ciphertexts: Vec<Ciphertext> = Vec::new(); self.pre_states.into_iter().map(move |pre| {
let mut new_commitments: Vec<Commitment> = Vec::new(); let post = self
let mut new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)> = Vec::new(); .post_states
.remove(&pre.account_id)
.expect("Account from pre states should exist in state diff");
(pre, post)
})
}
}
fn compute_circuit_output(
execution_state: ExecutionState,
visibility_mask: &[u8],
private_account_nonces: &[Nonce],
private_account_keys: &[(NullifierPublicKey, SharedSecretKey)],
private_account_nsks: &[NullifierSecretKey],
private_account_membership_proofs: &[Option<MembershipProof>],
) -> PrivacyPreservingCircuitOutput {
let mut output = PrivacyPreservingCircuitOutput {
public_pre_states: Vec::new(),
public_post_states: Vec::new(),
ciphertexts: Vec::new(),
new_commitments: Vec::new(),
new_nullifiers: Vec::new(),
};
let states_iter = execution_state.into_states_iter();
assert_eq!(
visibility_mask.len(),
states_iter.len(),
"Invalid visibility mask length"
);
let mut private_nonces_iter = private_account_nonces.iter(); let mut private_nonces_iter = private_account_nonces.iter();
let mut private_keys_iter = private_account_keys.iter(); let mut private_keys_iter = private_account_keys.iter();
@ -137,141 +249,156 @@ fn main() {
let mut private_membership_proofs_iter = private_account_membership_proofs.iter(); let mut private_membership_proofs_iter = private_account_membership_proofs.iter();
let mut output_index = 0; let mut output_index = 0;
for i in 0..n_accounts { for (visibility_mask, (pre_state, post_state)) in
match visibility_mask[i] { visibility_mask.iter().copied().zip(states_iter)
{
match visibility_mask {
0 => { 0 => {
// Public account // Public account
public_pre_states.push(pre_states[i].clone()); output.public_pre_states.push(pre_state);
output.public_post_states.push(post_state);
let mut post = state_diff.get(&pre_states[i].account_id).unwrap().clone();
if post.program_owner == DEFAULT_PROGRAM_ID {
// Claim account
post.program_owner = program_id;
}
public_post_states.push(post);
} }
1 | 2 => { 1 | 2 => {
let new_nonce = private_nonces_iter.next().expect("Missing private nonce"); let Some((npk, shared_secret)) = private_keys_iter.next() else {
let (npk, shared_secret) = private_keys_iter.next().expect("Missing keys"); panic!("Missing private account key");
};
if AccountId::from(npk) != pre_states[i].account_id { assert_eq!(
panic!("AccountId mismatch"); AccountId::from(npk),
} pre_state.account_id,
"AccountId mismatch"
);
if visibility_mask[i] == 1 { let new_nullifier = if visibility_mask == 1 {
// Private account with authentication // Private account with authentication
let nsk = private_nsks_iter.next().expect("Missing nsk");
let Some(nsk) = private_nsks_iter.next() else {
panic!("Missing private account nullifier secret key");
};
// Verify the nullifier public key // Verify the nullifier public key
let expected_npk = NullifierPublicKey::from(nsk); assert_eq!(
if &expected_npk != npk { npk,
panic!("Nullifier public key mismatch"); &NullifierPublicKey::from(nsk),
} "Nullifier public key mismatch"
);
// Check pre_state authorization // Check pre_state authorization
if !pre_states[i].is_authorized { assert!(
panic!("Pre-state not authorized"); pre_state.is_authorized,
} "Pre-state not authorized for authenticated private account"
);
let membership_proof_opt = private_membership_proofs_iter let Some(membership_proof_opt) = private_membership_proofs_iter.next() else {
.next() panic!("Missing membership proof");
.expect("Missing membership proof"); };
let (nullifier, set_digest) = membership_proof_opt
.as_ref()
.map(|membership_proof| {
// Compute commitment set digest associated with provided auth path
let commitment_pre = Commitment::new(npk, &pre_states[i].account);
let set_digest =
compute_digest_for_path(&commitment_pre, membership_proof);
// Compute update nullifier compute_nullifier_and_set_digest(
let nullifier = Nullifier::for_account_update(&commitment_pre, nsk); membership_proof_opt.as_ref(),
(nullifier, set_digest) &pre_state.account,
}) npk,
.unwrap_or_else(|| { nsk,
if pre_states[i].account != Account::default() { )
panic!("Found new private account with non default values.");
}
// Compute initialization nullifier
let nullifier = Nullifier::for_account_initialization(npk);
(nullifier, DUMMY_COMMITMENT_HASH)
});
new_nullifiers.push((nullifier, set_digest));
} else { } else {
// Private account without authentication // Private account without authentication
if pre_states[i].account != Account::default() {
panic!("Found new private account with non default values.");
}
if pre_states[i].is_authorized { assert_eq!(
panic!("Found new private account marked as authorized."); pre_state.account,
} Account::default(),
"Found new private account with non default values",
);
assert!(
!pre_state.is_authorized,
"Found new private account marked as authorized."
);
let Some(membership_proof_opt) = private_membership_proofs_iter.next() else {
panic!("Missing membership proof");
};
let membership_proof_opt = private_membership_proofs_iter
.next()
.expect("Missing membership proof");
assert!( assert!(
membership_proof_opt.is_none(), membership_proof_opt.is_none(),
"Membership proof must be None for unauthorized accounts" "Membership proof must be None for unauthorized accounts"
); );
let nullifier = Nullifier::for_account_initialization(npk); let nullifier = Nullifier::for_account_initialization(npk);
new_nullifiers.push((nullifier, DUMMY_COMMITMENT_HASH)); (nullifier, DUMMY_COMMITMENT_HASH)
} };
output.new_nullifiers.push(new_nullifier);
// Update post-state with new nonce // Update post-state with new nonce
let mut post_with_updated_values = let mut post_with_updated_nonce = post_state;
state_diff.get(&pre_states[i].account_id).unwrap().clone(); let Some(new_nonce) = private_nonces_iter.next() else {
post_with_updated_values.nonce = *new_nonce; panic!("Missing private account nonce");
};
if post_with_updated_values.program_owner == DEFAULT_PROGRAM_ID { post_with_updated_nonce.nonce = *new_nonce;
// Claim account
post_with_updated_values.program_owner = program_id;
}
// Compute commitment // Compute commitment
let commitment_post = Commitment::new(npk, &post_with_updated_values); let commitment_post = Commitment::new(npk, &post_with_updated_nonce);
// Encrypt and push post state // Encrypt and push post state
let encrypted_account = EncryptionScheme::encrypt( let encrypted_account = EncryptionScheme::encrypt(
&post_with_updated_values, &post_with_updated_nonce,
shared_secret, shared_secret,
&commitment_post, &commitment_post,
output_index, output_index,
); );
new_commitments.push(commitment_post); output.new_commitments.push(commitment_post);
ciphertexts.push(encrypted_account); output.ciphertexts.push(encrypted_account);
output_index += 1; output_index += 1;
} }
_ => panic!("Invalid visibility mask value"), _ => panic!("Invalid visibility mask value"),
} }
} }
if private_nonces_iter.next().is_some() { assert!(private_nonces_iter.next().is_none(), "Too many nonces");
panic!("Too many nonces");
}
if private_keys_iter.next().is_some() { assert!(
panic!("Too many private account keys"); private_keys_iter.next().is_none(),
} "Too many private account keys"
);
if private_nsks_iter.next().is_some() { assert!(
panic!("Too many private account authentication keys"); private_nsks_iter.next().is_none(),
} "Too many private account nullifier secret keys"
);
if private_membership_proofs_iter.next().is_some() { assert!(
panic!("Too many private account membership proofs"); private_membership_proofs_iter.next().is_none(),
} "Too many private account membership proofs"
);
let output = PrivacyPreservingCircuitOutput { output
public_pre_states, }
public_post_states,
ciphertexts, fn compute_nullifier_and_set_digest(
new_commitments, membership_proof_opt: Option<&MembershipProof>,
new_nullifiers, pre_account: &Account,
}; npk: &NullifierPublicKey,
nsk: &NullifierSecretKey,
env::commit(&output); ) -> (Nullifier, CommitmentSetDigest) {
membership_proof_opt
.as_ref()
.map(|membership_proof| {
// Compute commitment set digest associated with provided auth path
let commitment_pre = Commitment::new(npk, pre_account);
let set_digest = compute_digest_for_path(&commitment_pre, membership_proof);
// Compute update nullifier
let nullifier = Nullifier::for_account_update(&commitment_pre, nsk);
(nullifier, set_digest)
})
.unwrap_or_else(|| {
assert_eq!(
*pre_account,
Account::default(),
"Found new private account with non default values"
);
// Compute initialization nullifier
let nullifier = Nullifier::for_account_initialization(npk);
(nullifier, DUMMY_COMMITMENT_HASH)
})
} }

View File

@ -0,0 +1,38 @@
use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs};
type Instruction = (Option<Vec<u8>>, bool);
/// A program that optionally modifies the account data and optionally claims it.
fn main() {
let (
ProgramInput {
pre_states,
instruction: (data_opt, should_claim),
},
instruction_words,
) = read_nssa_inputs::<Instruction>();
let [pre] = match pre_states.try_into() {
Ok(array) => array,
Err(_) => return,
};
let account_pre = &pre.account;
let mut account_post = account_pre.clone();
// Update data if provided
if let Some(data) = data_opt {
account_post.data = data
.try_into()
.expect("provided data should fit into data limit");
}
// Claim or not based on the boolean flag
let post_state = if should_claim {
AccountPostState::new_claimed(account_post)
} else {
AccountPostState::new(account_post)
};
write_nssa_outputs(instruction_words, vec![pre], vec![post_state]);
}

View File

@ -0,0 +1,53 @@
use nssa_core::{
account::AccountWithMetadata,
program::{
AccountPostState, ChainedCall, ProgramId, ProgramInput, read_nssa_inputs,
write_nssa_outputs_with_chained_call,
},
};
use risc0_zkvm::serde::to_vec;
type Instruction = (u128, ProgramId);
/// A malicious test program that attempts to change authorization status.
/// It accepts two accounts and executes a native token transfer program via chain call,
/// but sets the `is_authorized` field of the first account to true.
fn main() {
let (
ProgramInput {
pre_states,
instruction: (balance, transfer_program_id),
},
instruction_words,
) = read_nssa_inputs::<Instruction>();
let [sender, receiver] = match pre_states.try_into() {
Ok(array) => array,
Err(_) => return,
};
// Maliciously set is_authorized to true for the first account
let authorised_sender = AccountWithMetadata {
is_authorized: true,
..sender.clone()
};
let instruction_data = to_vec(&balance).unwrap();
let chained_call = ChainedCall {
program_id: transfer_program_id,
instruction_data,
pre_states: vec![authorised_sender.clone(), receiver.clone()],
pda_seeds: vec![],
};
write_nssa_outputs_with_chained_call(
instruction_words,
vec![sender.clone(), receiver.clone()],
vec![
AccountPostState::new(sender.account),
AccountPostState::new(receiver.account),
],
vec![chained_call],
);
}

View File

@ -375,7 +375,7 @@ impl WalletCore {
pub async fn send_privacy_preserving_tx( pub async fn send_privacy_preserving_tx(
&self, &self,
accounts: Vec<PrivacyPreservingAccount>, accounts: Vec<PrivacyPreservingAccount>,
instruction_data: &InstructionData, instruction_data: InstructionData,
program: &ProgramWithDependencies, program: &ProgramWithDependencies,
) -> Result<(SendTxResponse, Vec<SharedSecretKey>), ExecutionFailureKind> { ) -> Result<(SendTxResponse, Vec<SharedSecretKey>), ExecutionFailureKind> {
self.send_privacy_preserving_tx_with_pre_check(accounts, instruction_data, program, |_| { self.send_privacy_preserving_tx_with_pre_check(accounts, instruction_data, program, |_| {
@ -387,7 +387,7 @@ impl WalletCore {
pub async fn send_privacy_preserving_tx_with_pre_check( pub async fn send_privacy_preserving_tx_with_pre_check(
&self, &self,
accounts: Vec<PrivacyPreservingAccount>, accounts: Vec<PrivacyPreservingAccount>,
instruction_data: &InstructionData, instruction_data: InstructionData,
program: &ProgramWithDependencies, program: &ProgramWithDependencies,
tx_pre_check: impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>, tx_pre_check: impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>,
) -> Result<(SendTxResponse, Vec<SharedSecretKey>), ExecutionFailureKind> { ) -> Result<(SendTxResponse, Vec<SharedSecretKey>), ExecutionFailureKind> {
@ -403,16 +403,16 @@ impl WalletCore {
let private_account_keys = acc_manager.private_account_keys(); let private_account_keys = acc_manager.private_account_keys();
let (output, proof) = nssa::privacy_preserving_transaction::circuit::execute_and_prove( let (output, proof) = nssa::privacy_preserving_transaction::circuit::execute_and_prove(
&pre_states, pre_states,
instruction_data, instruction_data,
acc_manager.visibility_mask(), acc_manager.visibility_mask().to_vec(),
&produce_random_nonces(private_account_keys.len()), produce_random_nonces(private_account_keys.len()),
&private_account_keys private_account_keys
.iter() .iter()
.map(|keys| (keys.npk.clone(), keys.ssk)) .map(|keys| (keys.npk.clone(), keys.ssk))
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
&acc_manager.private_account_auth(), acc_manager.private_account_auth(),
&acc_manager.private_account_membership_proofs(), acc_manager.private_account_membership_proofs(),
&program.to_owned(), &program.to_owned(),
) )
.unwrap(); .unwrap();

View File

@ -19,7 +19,7 @@ impl NativeTokenTransfer<'_> {
PrivacyPreservingAccount::PrivateOwned(from), PrivacyPreservingAccount::PrivateOwned(from),
PrivacyPreservingAccount::Public(to), PrivacyPreservingAccount::Public(to),
], ],
&instruction_data, instruction_data,
&program.into(), &program.into(),
tx_pre_check, tx_pre_check,
) )

View File

@ -17,7 +17,7 @@ impl NativeTokenTransfer<'_> {
self.0 self.0
.send_privacy_preserving_tx( .send_privacy_preserving_tx(
vec![PrivacyPreservingAccount::PrivateOwned(from)], vec![PrivacyPreservingAccount::PrivateOwned(from)],
&Program::serialize_instruction(instruction).unwrap(), Program::serialize_instruction(instruction).unwrap(),
&Program::authenticated_transfer_program().into(), &Program::authenticated_transfer_program().into(),
) )
.await .await
@ -46,7 +46,7 @@ impl NativeTokenTransfer<'_> {
ipk: to_ipk, ipk: to_ipk,
}, },
], ],
&instruction_data, instruction_data,
&program.into(), &program.into(),
tx_pre_check, tx_pre_check,
) )
@ -73,7 +73,7 @@ impl NativeTokenTransfer<'_> {
PrivacyPreservingAccount::PrivateOwned(from), PrivacyPreservingAccount::PrivateOwned(from),
PrivacyPreservingAccount::PrivateOwned(to), PrivacyPreservingAccount::PrivateOwned(to),
], ],
&instruction_data, instruction_data,
&program.into(), &program.into(),
tx_pre_check, tx_pre_check,
) )

View File

@ -20,7 +20,7 @@ impl NativeTokenTransfer<'_> {
PrivacyPreservingAccount::Public(from), PrivacyPreservingAccount::Public(from),
PrivacyPreservingAccount::PrivateOwned(to), PrivacyPreservingAccount::PrivateOwned(to),
], ],
&instruction_data, instruction_data,
&program.into(), &program.into(),
tx_pre_check, tx_pre_check,
) )
@ -52,7 +52,7 @@ impl NativeTokenTransfer<'_> {
ipk: to_ipk, ipk: to_ipk,
}, },
], ],
&instruction_data, instruction_data,
&program.into(), &program.into(),
tx_pre_check, tx_pre_check,
) )

View File

@ -37,7 +37,7 @@ impl Pinata<'_> {
PrivacyPreservingAccount::Public(pinata_account_id), PrivacyPreservingAccount::Public(pinata_account_id),
PrivacyPreservingAccount::PrivateOwned(winner_account_id), PrivacyPreservingAccount::PrivateOwned(winner_account_id),
], ],
&nssa::program::Program::serialize_instruction(solution).unwrap(), nssa::program::Program::serialize_instruction(solution).unwrap(),
&nssa::program::Program::pinata().into(), &nssa::program::Program::pinata().into(),
) )
.await .await

View File

@ -52,7 +52,7 @@ impl Token<'_> {
PrivacyPreservingAccount::Public(definition_account_id), PrivacyPreservingAccount::Public(definition_account_id),
PrivacyPreservingAccount::PrivateOwned(supply_account_id), PrivacyPreservingAccount::PrivateOwned(supply_account_id),
], ],
&instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
) )
.await .await
@ -82,7 +82,7 @@ impl Token<'_> {
PrivacyPreservingAccount::PrivateOwned(definition_account_id), PrivacyPreservingAccount::PrivateOwned(definition_account_id),
PrivacyPreservingAccount::Public(supply_account_id), PrivacyPreservingAccount::Public(supply_account_id),
], ],
&instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
) )
.await .await
@ -112,7 +112,7 @@ impl Token<'_> {
PrivacyPreservingAccount::PrivateOwned(definition_account_id), PrivacyPreservingAccount::PrivateOwned(definition_account_id),
PrivacyPreservingAccount::PrivateOwned(supply_account_id), PrivacyPreservingAccount::PrivateOwned(supply_account_id),
], ],
&instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
) )
.await .await
@ -180,7 +180,7 @@ impl Token<'_> {
PrivacyPreservingAccount::PrivateOwned(sender_account_id), PrivacyPreservingAccount::PrivateOwned(sender_account_id),
PrivacyPreservingAccount::PrivateOwned(recipient_account_id), PrivacyPreservingAccount::PrivateOwned(recipient_account_id),
], ],
&instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
) )
.await .await
@ -212,7 +212,7 @@ impl Token<'_> {
ipk: recipient_ipk, ipk: recipient_ipk,
}, },
], ],
&instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
) )
.await .await
@ -240,7 +240,7 @@ impl Token<'_> {
PrivacyPreservingAccount::PrivateOwned(sender_account_id), PrivacyPreservingAccount::PrivateOwned(sender_account_id),
PrivacyPreservingAccount::Public(recipient_account_id), PrivacyPreservingAccount::Public(recipient_account_id),
], ],
&instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
) )
.await .await
@ -269,7 +269,7 @@ impl Token<'_> {
PrivacyPreservingAccount::Public(sender_account_id), PrivacyPreservingAccount::Public(sender_account_id),
PrivacyPreservingAccount::PrivateOwned(recipient_account_id), PrivacyPreservingAccount::PrivateOwned(recipient_account_id),
], ],
&instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
) )
.await .await
@ -302,7 +302,7 @@ impl Token<'_> {
ipk: recipient_ipk, ipk: recipient_ipk,
}, },
], ],
&instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
) )
.await .await
@ -365,7 +365,7 @@ impl Token<'_> {
PrivacyPreservingAccount::PrivateOwned(definition_account_id), PrivacyPreservingAccount::PrivateOwned(definition_account_id),
PrivacyPreservingAccount::PrivateOwned(holder_account_id), PrivacyPreservingAccount::PrivateOwned(holder_account_id),
], ],
&instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
) )
.await .await
@ -393,7 +393,7 @@ impl Token<'_> {
PrivacyPreservingAccount::PrivateOwned(definition_account_id), PrivacyPreservingAccount::PrivateOwned(definition_account_id),
PrivacyPreservingAccount::Public(holder_account_id), PrivacyPreservingAccount::Public(holder_account_id),
], ],
&instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
) )
.await .await
@ -422,7 +422,7 @@ impl Token<'_> {
PrivacyPreservingAccount::Public(definition_account_id), PrivacyPreservingAccount::Public(definition_account_id),
PrivacyPreservingAccount::PrivateOwned(holder_account_id), PrivacyPreservingAccount::PrivateOwned(holder_account_id),
], ],
&instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
) )
.await .await
@ -491,7 +491,7 @@ impl Token<'_> {
PrivacyPreservingAccount::PrivateOwned(definition_account_id), PrivacyPreservingAccount::PrivateOwned(definition_account_id),
PrivacyPreservingAccount::PrivateOwned(holder_account_id), PrivacyPreservingAccount::PrivateOwned(holder_account_id),
], ],
&instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
) )
.await .await
@ -523,7 +523,7 @@ impl Token<'_> {
ipk: holder_ipk, ipk: holder_ipk,
}, },
], ],
&instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
) )
.await .await
@ -551,7 +551,7 @@ impl Token<'_> {
PrivacyPreservingAccount::PrivateOwned(definition_account_id), PrivacyPreservingAccount::PrivateOwned(definition_account_id),
PrivacyPreservingAccount::Public(holder_account_id), PrivacyPreservingAccount::Public(holder_account_id),
], ],
&instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
) )
.await .await
@ -580,7 +580,7 @@ impl Token<'_> {
PrivacyPreservingAccount::Public(definition_account_id), PrivacyPreservingAccount::Public(definition_account_id),
PrivacyPreservingAccount::PrivateOwned(holder_account_id), PrivacyPreservingAccount::PrivateOwned(holder_account_id),
], ],
&instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
) )
.await .await
@ -613,7 +613,7 @@ impl Token<'_> {
ipk: holder_ipk, ipk: holder_ipk,
}, },
], ],
&instruction_data, instruction_data,
&Program::token().into(), &Program::token().into(),
) )
.await .await