Merge branch 'main' into schouhy/generalize-npk-to-multiple-accounts

This commit is contained in:
Sergio Chouhy 2026-04-21 18:53:24 -03:00
commit 670527c2f1
39 changed files with 361 additions and 86 deletions

View File

@ -13,6 +13,7 @@ ignore = [
{ id = "RUSTSEC-2025-0055", reason = "`tracing-subscriber` v0.2.25 pulled in by ark-relations v0.4.0 - will be addressed before mainnet" },
{ id = "RUSTSEC-2025-0141", reason = "`bincode` is unmaintained but continuing to use it." },
{ id = "RUSTSEC-2023-0089", reason = "atomic-polyfill is pulled transitively via risc0-zkvm; waiting on upstream fix (see https://github.com/risc0/risc0/issues/3453)" },
{ id = "RUSTSEC-2026-0097", reason = "`rand` v0.8.5 is present transitively from logos crates, modification may break integration" },
]
yanked = "deny"
unused-ignored-advisory = "deny"

30
Cargo.lock generated
View File

@ -1715,15 +1715,6 @@ dependencies = [
"libc",
]
[[package]]
name = "core2"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505"
dependencies = [
"memchr",
]
[[package]]
name = "cpp_demangle"
version = "0.4.5"
@ -1968,7 +1959,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de"
dependencies = [
"data-encoding",
"syn 2.0.117",
"syn 1.0.109",
]
[[package]]
@ -5249,11 +5240,11 @@ dependencies = [
[[package]]
name = "multihash"
version = "0.19.3"
version = "0.19.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d"
checksum = "89ace881e3f514092ce9efbcb8f413d0ad9763860b828981c2de51ddc666936c"
dependencies = [
"core2",
"no_std_io2",
"unsigned-varint",
]
@ -5314,6 +5305,15 @@ dependencies = [
"zeroize",
]
[[package]]
name = "no_std_io2"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a3564ce7035b1e4778d8cb6cacebb5d766b5e8fe5a75b9e441e33fb61a872c6"
dependencies = [
"memchr",
]
[[package]]
name = "no_std_strings"
version = "0.1.3"
@ -7143,9 +7143,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.103.10"
version = "0.103.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06"
dependencies = [
"ring",
"rustls-pki-types",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -382,8 +382,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 +393,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,6 +408,75 @@ 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,
},
}
#[must_use]
pub fn compute_authorized_pdas(
caller_program_id: Option<ProgramId>,
@ -448,31 +517,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 +558,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 +571,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 +593,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 {

View File

@ -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 {

View File

@ -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)));
}

View File

@ -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,
@ -934,10 +937,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();
@ -946,7 +950,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]
@ -963,7 +974,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]
@ -980,7 +1001,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]
@ -1004,7 +1035,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]
@ -1028,7 +1064,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]
@ -1052,7 +1093,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]
@ -1076,16 +1122,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!(
@ -1104,7 +1155,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]
@ -1129,7 +1185,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]
@ -1147,7 +1208,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]
@ -1176,7 +1242,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 {
@ -3131,7 +3202,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
@ -3177,7 +3253,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);
@ -3425,7 +3516,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]

View File

@ -14,7 +14,7 @@ use nssa_core::{
use crate::{
V03State, ensure,
error::NssaError,
error::{InvalidProgramBehaviorError, NssaError},
privacy_preserving_transaction::{
PrivacyPreservingTransaction, circuit::Proof, message::Message,
},
@ -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,33 @@ 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);
ensure!(
account_id == pda,
InvalidProgramBehaviorError::MismatchedPdaClaim {
expected: pda,
actual: account_id
}
);
}
}
@ -238,7 +257,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 +265,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 }
);
}

View File

@ -125,12 +125,17 @@ 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)));