fix: check public account authorization in privacy preserving circuit

This commit is contained in:
Daniil Polyakov
2026-01-17 02:28:31 +03:00
parent 78a6ff72ee
commit 67d5a3e9f4
26 changed files with 201 additions and 43 deletions
+14
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 {
fn from(value: (&ProgramId, &PdaSeed)) -> Self {
use risc0_zkvm::sha::{Impl, Sha256};
@@ -43,7 +43,7 @@ impl From<Program> for ProgramWithDependencies {
}
/// 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")]
pub fn execute_and_prove(
pre_states: Vec<AccountWithMetadata>,
@@ -87,8 +87,6 @@ pub fn execute_and_prove(
.decode()
.map_err(|e| NssaError::ProgramOutputDeserializationError(e.to_string()))?;
// TODO: Why private execution doesn't care about public account authorization?
// TODO: remove clone
program_outputs.push(program_output.clone());
+11
View File
@@ -235,6 +235,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 {
use test_program_methods::MODIFIED_TRANSFER_ELF;
// This unwrap won't panic since the `MODIFIED_TRANSFER_ELF` comes from risc0 build of
+5 -17
View File
@@ -4,7 +4,7 @@ use borsh::{BorshDeserialize, BorshSerialize};
use log::debug;
use nssa_core::{
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};
@@ -135,8 +135,10 @@ impl PublicTransaction {
chained_call.program_id, program_output
);
let authorized_pdas =
Self::compute_authorized_pdas(&caller_program_id, &chained_call.pda_seeds);
let authorized_pdas = nssa_core::program::compute_authorized_pdas(
caller_program_id,
&chained_call.pda_seeds,
);
for pre in &program_output.pre_states {
let account_id = pre.account_id;
@@ -200,20 +202,6 @@ impl PublicTransaction {
Ok(state_diff)
}
fn compute_authorized_pdas(
caller_program_id: &Option<ProgramId>,
pda_seeds: &[PdaSeed],
) -> HashSet<AccountId> {
if let Some(caller_program_id) = caller_program_id {
pda_seeds
.iter()
.map(|pda_seed| AccountId::from((caller_program_id, pda_seed)))
.collect()
} else {
HashSet::new()
}
}
}
#[cfg(test)]
+56
View File
@@ -4369,4 +4369,60 @@ pub mod tests {
assert!(matches!(res, 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(_))));
}
}