refactor to enforce validation of state diff through constructors

This commit is contained in:
Sergio Chouhy 2026-04-02 02:39:37 -03:00
parent 40c7b308a9
commit 9915f09d6d
7 changed files with 463 additions and 428 deletions

View File

@ -20,7 +20,7 @@ pub use state::{
CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID,
CLOCK_PROGRAM_ACCOUNT_IDS, V03State,
};
pub use state_diff::ValidatedStateDiff;
pub use validated_state_diff::ValidatedStateDiff;
pub mod encoding;
pub mod error;
@ -31,7 +31,7 @@ pub mod program_deployment_transaction;
pub mod public_transaction;
mod signature;
mod state;
mod state_diff;
mod validated_state_diff;
pub mod program_methods {
include!(concat!(env!("OUT_DIR"), "/program_methods/mod.rs"));

View File

@ -1,19 +1,10 @@
use std::{
collections::HashSet, hash::Hash,
};
use std::collections::HashSet;
use borsh::{BorshDeserialize, BorshSerialize};
use nssa_core::{
BlockId, PrivacyPreservingCircuitOutput, Timestamp,
account::{Account, AccountWithMetadata},
};
use nssa_core::account::{Account, AccountId};
use sha2::{Digest as _, digest::FixedOutput as _};
use super::{message::Message, witness_set::WitnessSet};
use crate::{
AccountId, V03State, error::NssaError, privacy_preserving_transaction::circuit::Proof,
state_diff::ValidatedStateDiff,
};
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct PrivacyPreservingTransaction {
@ -30,120 +21,6 @@ impl PrivacyPreservingTransaction {
}
}
pub fn validate_and_produce_public_state_diff(
&self,
state: &V03State,
block_id: BlockId,
timestamp: Timestamp,
) -> Result<ValidatedStateDiff, NssaError> {
let message = &self.message;
let witness_set = &self.witness_set;
// 1. Commitments or nullifiers are non empty
if message.new_commitments.is_empty() && message.new_nullifiers.is_empty() {
return Err(NssaError::InvalidInput(
"Empty commitments and empty nullifiers found in message".into(),
));
}
// 2. Check there are no duplicate account_ids in the public_account_ids list.
if n_unique(&message.public_account_ids) != message.public_account_ids.len() {
return Err(NssaError::InvalidInput(
"Duplicate account_ids found in message".into(),
));
}
// Check there are no duplicate nullifiers in the new_nullifiers list
if n_unique(&message.new_nullifiers) != message.new_nullifiers.len() {
return Err(NssaError::InvalidInput(
"Duplicate nullifiers found in message".into(),
));
}
// Check there are no duplicate commitments in the new_commitments list
if n_unique(&message.new_commitments) != message.new_commitments.len() {
return Err(NssaError::InvalidInput(
"Duplicate commitments found in message".into(),
));
}
// 3. Nonce checks and Valid signatures
// Check exactly one nonce is provided for each signature
if message.nonces.len() != witness_set.signatures_and_public_keys.len() {
return Err(NssaError::InvalidInput(
"Mismatch between number of nonces and signatures/public keys".into(),
));
}
// Check the signatures are valid
if !witness_set.signatures_are_valid_for(message) {
return Err(NssaError::InvalidInput(
"Invalid signature for given message and public key".into(),
));
}
let signer_account_ids = self.signer_account_ids();
// Check nonces corresponds to the current nonces on the public state.
for (account_id, nonce) in signer_account_ids.iter().zip(&message.nonces) {
let current_nonce = state.get_account_by_id(*account_id).nonce;
if current_nonce != *nonce {
return Err(NssaError::InvalidInput("Nonce mismatch".into()));
}
}
// Verify validity window
if !message.block_validity_window.is_valid_for(block_id)
|| !message.timestamp_validity_window.is_valid_for(timestamp)
{
return Err(NssaError::OutOfValidityWindow);
}
// Build pre_states for proof verification
let public_pre_states: Vec<_> = message
.public_account_ids
.iter()
.map(|account_id| {
AccountWithMetadata::new(
state.get_account_by_id(*account_id),
signer_account_ids.contains(account_id),
*account_id,
)
})
.collect();
// 4. Proof verification
check_privacy_preserving_circuit_proof_is_valid(
&witness_set.proof,
&public_pre_states,
message,
)?;
// 5. Commitment freshness
state.check_commitments_are_new(&message.new_commitments)?;
// 6. Nullifier uniqueness
state.check_nullifiers_are_valid(&message.new_nullifiers)?;
let public_diff = message
.public_account_ids
.iter()
.copied()
.zip(message.public_post_states.clone())
.collect();
let new_nullifiers = message
.new_nullifiers
.iter()
.cloned()
.map(|(nullifier, _)| nullifier)
.collect();
Ok(ValidatedStateDiff::new(
self.signer_account_ids(),
public_diff,
message.new_commitments.clone(),
new_nullifiers,
))
}
#[must_use]
pub const fn message(&self) -> &Message {
&self.message
@ -193,36 +70,6 @@ impl PrivacyPreservingTransaction {
}
}
fn check_privacy_preserving_circuit_proof_is_valid(
proof: &Proof,
public_pre_states: &[AccountWithMetadata],
message: &Message,
) -> Result<(), NssaError> {
let output = PrivacyPreservingCircuitOutput {
public_pre_states: public_pre_states.to_vec(),
public_post_states: message.public_post_states.clone(),
ciphertexts: message
.encrypted_private_post_states
.iter()
.cloned()
.map(|value| value.ciphertext)
.collect(),
new_commitments: message.new_commitments.clone(),
new_nullifiers: message.new_nullifiers.clone(),
block_validity_window: message.block_validity_window,
timestamp_validity_window: message.timestamp_validity_window,
};
proof
.is_valid_for(&output)
.then_some(())
.ok_or(NssaError::InvalidPrivacyPreservingProof)
}
fn n_unique<T: Eq + Hash>(data: &[T]) -> usize {
let set: HashSet<&T> = data.iter().collect();
set.len()
}
#[cfg(test)]
mod tests {
use crate::{

View File

@ -2,9 +2,7 @@ use borsh::{BorshDeserialize, BorshSerialize};
use nssa_core::account::AccountId;
use sha2::{Digest as _, digest::FixedOutput as _};
use crate::{
V03State, error::NssaError, program::Program, program_deployment_transaction::message::Message,
};
use crate::program_deployment_transaction::message::Message;
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct ProgramDeploymentTransaction {
@ -22,19 +20,6 @@ impl ProgramDeploymentTransaction {
self.message
}
pub(crate) fn validate_and_produce_public_state_diff(
&self,
state: &V03State,
) -> Result<Program, NssaError> {
// TODO: remove clone
let program = Program::new(self.message.bytecode.clone())?;
if state.programs().contains_key(&program.id()) {
Err(NssaError::ProgramAlreadyExists)
} else {
Ok(program)
}
}
#[must_use]
pub fn hash(&self) -> [u8; 32] {
let bytes = self.to_bytes();

View File

@ -1,21 +1,10 @@
use std::collections::{HashMap, HashSet, VecDeque};
use std::collections::HashSet;
use borsh::{BorshDeserialize, BorshSerialize};
use log::debug;
use nssa_core::{
BlockId, Timestamp,
account::{Account, AccountId, AccountWithMetadata},
program::{ChainedCall, Claim, DEFAULT_PROGRAM_ID, validate_execution},
};
use nssa_core::account::AccountId;
use sha2::{Digest as _, digest::FixedOutput as _};
use crate::{
V03State, ensure,
error::NssaError,
public_transaction::{Message, WitnessSet},
state::MAX_NUMBER_CHAINED_CALLS,
state_diff::ValidatedStateDiff,
};
use crate::public_transaction::{Message, WitnessSet};
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct PublicTransaction {
@ -68,211 +57,6 @@ impl PublicTransaction {
hasher.update(&bytes);
hasher.finalize_fixed().into()
}
pub fn validate_and_produce_public_state_diff(
&self,
state: &V03State,
block_id: BlockId,
timestamp: Timestamp,
) -> Result<ValidatedStateDiff, NssaError> {
let message = self.message();
let witness_set = self.witness_set();
// All account_ids must be different
ensure!(
message.account_ids.iter().collect::<HashSet<_>>().len() == message.account_ids.len(),
NssaError::InvalidInput("Duplicate account_ids found in message".into(),)
);
// Check exactly one nonce is provided for each signature
ensure!(
message.nonces.len() == witness_set.signatures_and_public_keys.len(),
NssaError::InvalidInput(
"Mismatch between number of nonces and signatures/public keys".into(),
)
);
// Check the signatures are valid
ensure!(
witness_set.is_valid_for(message),
NssaError::InvalidInput("Invalid signature for given message and public key".into())
);
let signer_account_ids = self.signer_account_ids();
// Check nonces corresponds to the current nonces on the public state.
for (account_id, nonce) in signer_account_ids.iter().zip(&message.nonces) {
let current_nonce = state.get_account_by_id(*account_id).nonce;
ensure!(
current_nonce == *nonce,
NssaError::InvalidInput("Nonce mismatch".into())
);
}
// Build pre_states for execution
let input_pre_states: Vec<_> = message
.account_ids
.iter()
.map(|account_id| {
AccountWithMetadata::new(
state.get_account_by_id(*account_id),
signer_account_ids.contains(account_id),
*account_id,
)
})
.collect();
let mut state_diff: HashMap<AccountId, Account> = HashMap::new();
let initial_call = ChainedCall {
program_id: message.program_id,
instruction_data: message.instruction_data.clone(),
pre_states: input_pre_states,
pda_seeds: vec![],
};
let mut chained_calls = VecDeque::from_iter([(initial_call, None)]);
let mut chain_calls_counter = 0;
while let Some((chained_call, caller_program_id)) = chained_calls.pop_front() {
ensure!(
chain_calls_counter <= MAX_NUMBER_CHAINED_CALLS,
NssaError::MaxChainedCallsDepthExceeded
);
// Check that the `program_id` corresponds to a deployed program
let Some(program) = state.programs().get(&chained_call.program_id) else {
return Err(NssaError::InvalidInput("Unknown program".into()));
};
debug!(
"Program {:?} pre_states: {:?}, instruction_data: {:?}",
chained_call.program_id, chained_call.pre_states, chained_call.instruction_data
);
let mut program_output =
program.execute(&chained_call.pre_states, &chained_call.instruction_data)?;
debug!(
"Program {:?} output: {:?}",
chained_call.program_id, program_output
);
let authorized_pdas = nssa_core::program::compute_authorized_pdas(
caller_program_id,
&chained_call.pda_seeds,
);
let is_authorized = |account_id: &AccountId| {
signer_account_ids.contains(account_id) || authorized_pdas.contains(account_id)
};
for pre in &program_output.pre_states {
let account_id = pre.account_id;
// 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.
let expected_pre = state_diff
.get(&account_id)
.cloned()
.unwrap_or_else(|| state.get_account_by_id(account_id));
ensure!(
pre.account == expected_pre,
NssaError::InvalidProgramBehavior
);
// Check that authorization flags are consistent with the provided ones or
// authorized by program through the PDA mechanism
ensure!(
pre.is_authorized == is_authorized(&account_id),
NssaError::InvalidProgramBehavior
);
}
// 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
);
// Verify validity window
ensure!(
program_output.block_validity_window.is_valid_for(block_id)
&& program_output
.timestamp_validity_window
.is_valid_for(timestamp),
NssaError::OutOfValidityWindow
);
for (i, post) in program_output.post_states.iter_mut().enumerate() {
let Some(claim) = post.required_claim() else {
continue;
};
// The invoked program can only claim accounts with default program id.
ensure!(
post.account().program_owner == DEFAULT_PROGRAM_ID,
NssaError::InvalidProgramBehavior
);
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
);
}
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);
}
}
post.account_mut().program_owner = chained_call.program_id;
}
// Update the state diff
for (pre, post) in program_output
.pre_states
.iter()
.zip(program_output.post_states.iter())
{
state_diff.insert(pre.account_id, post.account().clone());
}
for new_call in program_output.chained_calls.into_iter().rev() {
chained_calls.push_front((new_call, Some(chained_call.program_id)));
}
chain_calls_counter = chain_calls_counter
.checked_add(1)
.expect("we check the max depth at the beginning of the loop");
}
// 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);
if pre.program_owner != DEFAULT_PROGRAM_ID {
return None;
}
if pre == *post {
return None;
}
Some(post)
}) {
ensure!(
post.program_owner != DEFAULT_PROGRAM_ID,
NssaError::InvalidProgramBehavior
);
}
Ok(ValidatedStateDiff::new(signer_account_ids, state_diff, vec![], vec![]))
}
}
#[cfg(test)]
@ -284,6 +68,7 @@ pub mod tests {
error::NssaError,
program::Program,
public_transaction::{Message, WitnessSet},
state_diff::ValidatedStateDiff,
};
fn keys_for_tests() -> (PrivateKey, PrivateKey, AccountId, AccountId) {
@ -392,7 +177,7 @@ pub mod tests {
let witness_set = WitnessSet::for_message(&message, &[&key1, &key1]);
let tx = PublicTransaction::new(message, witness_set);
let result = tx.validate_and_produce_public_state_diff(&state, 1, 0);
let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
}
@ -412,7 +197,7 @@ pub mod tests {
let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]);
let tx = PublicTransaction::new(message, witness_set);
let result = tx.validate_and_produce_public_state_diff(&state, 1, 0);
let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
}
@ -433,7 +218,7 @@ pub mod tests {
let mut witness_set = WitnessSet::for_message(&message, &[&key1, &key2]);
witness_set.signatures_and_public_keys[0].0 = Signature::new_for_tests([1; 64]);
let tx = PublicTransaction::new(message, witness_set);
let result = tx.validate_and_produce_public_state_diff(&state, 1, 0);
let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
}
@ -453,7 +238,7 @@ pub mod tests {
let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]);
let tx = PublicTransaction::new(message, witness_set);
let result = tx.validate_and_produce_public_state_diff(&state, 1, 0);
let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
}
@ -469,7 +254,7 @@ pub mod tests {
let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]);
let tx = PublicTransaction::new(message, witness_set);
let result = tx.validate_and_produce_public_state_diff(&state, 1, 0);
let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
}
}

View File

@ -19,7 +19,7 @@ use crate::{
privacy_preserving_transaction::PrivacyPreservingTransaction, program::Program,
program_deployment_transaction::ProgramDeploymentTransaction,
public_transaction::PublicTransaction,
state_diff::ValidatedStateDiff,
validated_state_diff::ValidatedStateDiff,
};
pub const MAX_NUMBER_CHAINED_CALLS: usize = 10;
@ -198,6 +198,9 @@ impl V03State {
}
self.private_state.0.extend(&diff.new_commitments);
self.private_state.1.extend(&diff.new_nullifiers);
if let Some(program) = diff.program {
self.insert_program(program);
}
}
pub fn transition_from_public_transaction(
@ -206,7 +209,7 @@ impl V03State {
block_id: BlockId,
timestamp: Timestamp,
) -> Result<(), NssaError> {
let diff = tx.validate_and_produce_public_state_diff(self, block_id, timestamp)?;
let diff = ValidatedStateDiff::from_public_transaction(tx, self, block_id, timestamp)?;
self.apply_state_diff(diff);
Ok(())
}
@ -217,7 +220,8 @@ impl V03State {
block_id: BlockId,
timestamp: Timestamp,
) -> Result<(), NssaError> {
let diff = tx.validate_and_produce_public_state_diff(self, block_id, timestamp)?;
let diff =
ValidatedStateDiff::from_privacy_preserving_transaction(tx, self, block_id, timestamp)?;
self.apply_state_diff(diff);
Ok(())
}
@ -226,8 +230,8 @@ impl V03State {
&mut self,
tx: &ProgramDeploymentTransaction,
) -> Result<(), NssaError> {
let program = tx.validate_and_produce_public_state_diff(self)?;
self.insert_program(program);
let diff = ValidatedStateDiff::from_program_deployment_transaction(tx, self)?;
self.apply_state_diff(diff);
Ok(())
}

View File

@ -0,0 +1,421 @@
use std::{
collections::{HashMap, HashSet, VecDeque},
hash::Hash,
};
use log::debug;
use nssa_core::{
BlockId, Commitment, Nullifier, PrivacyPreservingCircuitOutput, Timestamp,
account::{Account, AccountId, AccountWithMetadata},
program::{ChainedCall, Claim, DEFAULT_PROGRAM_ID, compute_authorized_pdas, validate_execution},
};
use crate::{
V03State, ensure,
error::NssaError,
privacy_preserving_transaction::{
PrivacyPreservingTransaction,
circuit::Proof,
message::Message,
},
program::Program,
program_deployment_transaction::ProgramDeploymentTransaction,
public_transaction::PublicTransaction,
state::MAX_NUMBER_CHAINED_CALLS,
};
/// The validated output of executing or verifying a transaction, ready to be applied to the state.
///
/// Can only be constructed by the transaction validation functions inside this crate, ensuring the
/// diff has been cryptographically checked before any state mutation occurs.
pub struct ValidatedStateDiff {
pub(crate) signer_account_ids: Vec<AccountId>,
pub(crate) public_diff: HashMap<AccountId, Account>,
pub(crate) new_commitments: Vec<Commitment>,
pub(crate) new_nullifiers: Vec<Nullifier>,
pub(crate) program: Option<Program>,
}
impl ValidatedStateDiff {
pub fn from_public_transaction(
tx: &PublicTransaction,
state: &V03State,
block_id: BlockId,
timestamp: Timestamp,
) -> Result<Self, NssaError> {
let message = tx.message();
let witness_set = tx.witness_set();
// All account_ids must be different
ensure!(
message.account_ids.iter().collect::<HashSet<_>>().len() == message.account_ids.len(),
NssaError::InvalidInput("Duplicate account_ids found in message".into(),)
);
// Check exactly one nonce is provided for each signature
ensure!(
message.nonces.len() == witness_set.signatures_and_public_keys.len(),
NssaError::InvalidInput(
"Mismatch between number of nonces and signatures/public keys".into(),
)
);
// Check the signatures are valid
ensure!(
witness_set.is_valid_for(message),
NssaError::InvalidInput("Invalid signature for given message and public key".into())
);
let signer_account_ids = tx.signer_account_ids();
// Check nonces corresponds to the current nonces on the public state.
for (account_id, nonce) in signer_account_ids.iter().zip(&message.nonces) {
let current_nonce = state.get_account_by_id(*account_id).nonce;
ensure!(
current_nonce == *nonce,
NssaError::InvalidInput("Nonce mismatch".into())
);
}
// Build pre_states for execution
let input_pre_states: Vec<_> = message
.account_ids
.iter()
.map(|account_id| {
AccountWithMetadata::new(
state.get_account_by_id(*account_id),
signer_account_ids.contains(account_id),
*account_id,
)
})
.collect();
let mut state_diff: HashMap<AccountId, Account> = HashMap::new();
let initial_call = ChainedCall {
program_id: message.program_id,
instruction_data: message.instruction_data.clone(),
pre_states: input_pre_states,
pda_seeds: vec![],
};
let mut chained_calls = VecDeque::from_iter([(initial_call, None)]);
let mut chain_calls_counter = 0;
while let Some((chained_call, caller_program_id)) = chained_calls.pop_front() {
ensure!(
chain_calls_counter <= MAX_NUMBER_CHAINED_CALLS,
NssaError::MaxChainedCallsDepthExceeded
);
// Check that the `program_id` corresponds to a deployed program
let Some(program) = state.programs().get(&chained_call.program_id) else {
return Err(NssaError::InvalidInput("Unknown program".into()));
};
debug!(
"Program {:?} pre_states: {:?}, instruction_data: {:?}",
chained_call.program_id, chained_call.pre_states, chained_call.instruction_data
);
let mut program_output =
program.execute(&chained_call.pre_states, &chained_call.instruction_data)?;
debug!(
"Program {:?} output: {:?}",
chained_call.program_id, program_output
);
let authorized_pdas =
compute_authorized_pdas(caller_program_id, &chained_call.pda_seeds);
let is_authorized = |account_id: &AccountId| {
signer_account_ids.contains(account_id) || authorized_pdas.contains(account_id)
};
for pre in &program_output.pre_states {
let account_id = pre.account_id;
// 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.
let expected_pre = state_diff
.get(&account_id)
.cloned()
.unwrap_or_else(|| state.get_account_by_id(account_id));
ensure!(
pre.account == expected_pre,
NssaError::InvalidProgramBehavior
);
// Check that authorization flags are consistent with the provided ones or
// authorized by program through the PDA mechanism
ensure!(
pre.is_authorized == is_authorized(&account_id),
NssaError::InvalidProgramBehavior
);
}
// 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
);
// Verify validity window
ensure!(
program_output.block_validity_window.is_valid_for(block_id)
&& program_output
.timestamp_validity_window
.is_valid_for(timestamp),
NssaError::OutOfValidityWindow
);
for (i, post) in program_output.post_states.iter_mut().enumerate() {
let Some(claim) = post.required_claim() else {
continue;
};
// The invoked program can only claim accounts with default program id.
ensure!(
post.account().program_owner == DEFAULT_PROGRAM_ID,
NssaError::InvalidProgramBehavior
);
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
);
}
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);
}
}
post.account_mut().program_owner = chained_call.program_id;
}
// Update the state diff
for (pre, post) in program_output
.pre_states
.iter()
.zip(program_output.post_states.iter())
{
state_diff.insert(pre.account_id, post.account().clone());
}
for new_call in program_output.chained_calls.into_iter().rev() {
chained_calls.push_front((new_call, Some(chained_call.program_id)));
}
chain_calls_counter = chain_calls_counter
.checked_add(1)
.expect("we check the max depth at the beginning of the loop");
}
// 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);
if pre.program_owner != DEFAULT_PROGRAM_ID {
return None;
}
if pre == *post {
return None;
}
Some(post)
}) {
ensure!(
post.program_owner != DEFAULT_PROGRAM_ID,
NssaError::InvalidProgramBehavior
);
}
Ok(Self {
signer_account_ids,
public_diff: state_diff,
new_commitments: vec![],
new_nullifiers: vec![],
program: None,
})
}
pub fn from_privacy_preserving_transaction(
tx: &PrivacyPreservingTransaction,
state: &V03State,
block_id: BlockId,
timestamp: Timestamp,
) -> Result<Self, NssaError> {
let message = &tx.message;
let witness_set = &tx.witness_set;
// 1. Commitments or nullifiers are non empty
if message.new_commitments.is_empty() && message.new_nullifiers.is_empty() {
return Err(NssaError::InvalidInput(
"Empty commitments and empty nullifiers found in message".into(),
));
}
// 2. Check there are no duplicate account_ids in the public_account_ids list.
if n_unique(&message.public_account_ids) != message.public_account_ids.len() {
return Err(NssaError::InvalidInput(
"Duplicate account_ids found in message".into(),
));
}
// Check there are no duplicate nullifiers in the new_nullifiers list
if n_unique(&message.new_nullifiers) != message.new_nullifiers.len() {
return Err(NssaError::InvalidInput(
"Duplicate nullifiers found in message".into(),
));
}
// Check there are no duplicate commitments in the new_commitments list
if n_unique(&message.new_commitments) != message.new_commitments.len() {
return Err(NssaError::InvalidInput(
"Duplicate commitments found in message".into(),
));
}
// 3. Nonce checks and Valid signatures
// Check exactly one nonce is provided for each signature
if message.nonces.len() != witness_set.signatures_and_public_keys.len() {
return Err(NssaError::InvalidInput(
"Mismatch between number of nonces and signatures/public keys".into(),
));
}
// Check the signatures are valid
if !witness_set.signatures_are_valid_for(message) {
return Err(NssaError::InvalidInput(
"Invalid signature for given message and public key".into(),
));
}
let signer_account_ids = tx.signer_account_ids();
// Check nonces corresponds to the current nonces on the public state.
for (account_id, nonce) in signer_account_ids.iter().zip(&message.nonces) {
let current_nonce = state.get_account_by_id(*account_id).nonce;
if current_nonce != *nonce {
return Err(NssaError::InvalidInput("Nonce mismatch".into()));
}
}
// Verify validity window
if !message.block_validity_window.is_valid_for(block_id)
|| !message.timestamp_validity_window.is_valid_for(timestamp)
{
return Err(NssaError::OutOfValidityWindow);
}
// Build pre_states for proof verification
let public_pre_states: Vec<_> = message
.public_account_ids
.iter()
.map(|account_id| {
AccountWithMetadata::new(
state.get_account_by_id(*account_id),
signer_account_ids.contains(account_id),
*account_id,
)
})
.collect();
// 4. Proof verification
check_privacy_preserving_circuit_proof_is_valid(
&witness_set.proof,
&public_pre_states,
message,
)?;
// 5. Commitment freshness
state.check_commitments_are_new(&message.new_commitments)?;
// 6. Nullifier uniqueness
state.check_nullifiers_are_valid(&message.new_nullifiers)?;
let public_diff = message
.public_account_ids
.iter()
.copied()
.zip(message.public_post_states.clone())
.collect();
let new_nullifiers = message
.new_nullifiers
.iter()
.cloned()
.map(|(nullifier, _)| nullifier)
.collect();
Ok(Self {
signer_account_ids,
public_diff,
new_commitments: message.new_commitments.clone(),
new_nullifiers,
program: None,
})
}
pub fn from_program_deployment_transaction(
tx: &ProgramDeploymentTransaction,
state: &V03State,
) -> Result<Self, NssaError> {
// TODO: remove clone
let program = Program::new(tx.message.bytecode.clone())?;
if state.programs().contains_key(&program.id()) {
return Err(NssaError::ProgramAlreadyExists);
}
Ok(Self {
signer_account_ids: vec![],
public_diff: HashMap::new(),
new_commitments: vec![],
new_nullifiers: vec![],
program: Some(program),
})
}
/// Returns the public account changes produced by this transaction.
///
/// Used by callers (e.g. the sequencer) to inspect the diff before committing it, for example
/// to enforce that system accounts are not modified by user transactions.
#[must_use]
pub fn public_diff(&self) -> &HashMap<AccountId, Account> {
&self.public_diff
}
}
fn check_privacy_preserving_circuit_proof_is_valid(
proof: &Proof,
public_pre_states: &[AccountWithMetadata],
message: &Message,
) -> Result<(), NssaError> {
let output = PrivacyPreservingCircuitOutput {
public_pre_states: public_pre_states.to_vec(),
public_post_states: message.public_post_states.clone(),
ciphertexts: message
.encrypted_private_post_states
.iter()
.cloned()
.map(|value| value.ciphertext)
.collect(),
new_commitments: message.new_commitments.clone(),
new_nullifiers: message.new_nullifiers.clone(),
block_validity_window: message.block_validity_window,
timestamp_validity_window: message.timestamp_validity_window,
};
proof
.is_valid_for(&output)
.then_some(())
.ok_or(NssaError::InvalidPrivacyPreservingProof)
}
fn n_unique<T: Eq + Hash>(data: &[T]) -> usize {
let set: HashSet<&T> = data.iter().collect();
set.len()
}

View File

@ -209,15 +209,22 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
transaction: &NSSATransaction,
block_id: BlockId,
timestamp: Timestamp,
) -> Option<Result<ValidatedStateDiff, nssa::error::NssaError>> {
) -> Result<ValidatedStateDiff, nssa::error::NssaError> {
match transaction {
NSSATransaction::Public(public_transaction) => Some(
public_transaction.validate_and_produce_public_state_diff(&self.state, block_id, timestamp),
),
NSSATransaction::PrivacyPreserving(privacy_preserving_transaction) => Some(
privacy_preserving_transaction.validate_and_produce_public_state_diff(&self.state, block_id, timestamp),
),
NSSATransaction::ProgramDeployment(_) => None,
NSSATransaction::Public(tx) => {
ValidatedStateDiff::from_public_transaction(tx, &self.state, block_id, timestamp)
}
NSSATransaction::PrivacyPreserving(tx) => {
ValidatedStateDiff::from_privacy_preserving_transaction(
tx,
&self.state,
block_id,
timestamp,
)
}
NSSATransaction::ProgramDeployment(tx) => {
ValidatedStateDiff::from_program_deployment_transaction(tx, &self.state)
}
}
}
@ -253,7 +260,7 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
let tx_hash = tx.hash();
let validated_diff = match self.validate_transaction_and_produce_state_diff(&tx, new_block_height, new_block_timestamp) {
Some(Ok(diff)) => {
Ok(diff) => {
let touches_system = clock_accounts_pre
.iter()
.any(|(id, pre)| diff.public_diff().get(id).is_some_and(|post| post != pre));
@ -263,15 +270,14 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
);
continue;
}
Some(diff)
diff
}
Some(Err(err)) => {
Err(err) => {
error!(
"Transaction with hash {tx_hash} failed execution check with error: {err:#?}, skipping it",
);
continue;
}
None => None,
};
// Check if block size exceeds limit
@ -299,20 +305,7 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
break;
}
match validated_diff {
Some(diff) => self.state.apply_state_diff(diff),
None => {
if let NSSATransaction::ProgramDeployment(deploy_tx) = &tx {
if let Err(err) = self.state.transition_from_program_deployment_transaction(deploy_tx) {
error!(
"Transaction with hash {tx_hash} failed execution check with error: {err:#?}, skipping it",
);
// TODO: Probably need to handle unsuccessful transaction execution?
continue;
}
}
}
}
self.state.apply_state_diff(validated_diff);
valid_transactions.push(tx);
info!("Validated transaction with hash {tx_hash}, including it in block");