diff --git a/common/src/transaction.rs b/common/src/transaction.rs index 2c004d6b..8dbd6365 100644 --- a/common/src/transaction.rs +++ b/common/src/transaction.rs @@ -61,28 +61,6 @@ impl NSSATransaction { )) } - /// Returns `true` if this transaction is a user invocation of the clock program. - /// - /// For public transactions: checks whether the program ID matches the clock program. - /// For privacy-preserving transactions: checks whether any clock account has a modified - /// post-state (i.e. `post != pre`), using the provided pre-state snapshot. - /// Pass an empty slice when only the public case is relevant (e.g. in committed blocks where - /// PP clock-touching transactions are already filtered out by the sequencer). - #[must_use] - pub fn is_invocation_of_clock_program( - &self, - clock_pre_states: &[(nssa::AccountId, nssa::Account)], - ) -> bool { - let clock_program_id = nssa::program::Program::clock().id(); - match self { - Self::Public(tx) => tx.message().program_id == clock_program_id, - Self::PrivacyPreserving(pp) => clock_pre_states - .iter() - .any(|(id, pre)| pp.public_post_state_for(id).is_some_and(|post| post != pre)), - Self::ProgramDeployment(_) => false, - } - } - // TODO: Introduce type-safe wrapper around checked transaction, e.g. AuthenticatedTransaction pub fn transaction_stateless_check(self) -> Result { // Stateless checks here diff --git a/indexer/core/src/block_store.rs b/indexer/core/src/block_store.rs index cfec9a01..bd1992f7 100644 --- a/indexer/core/src/block_store.rs +++ b/indexer/core/src/block_store.rs @@ -120,29 +120,6 @@ impl IndexerStore { pub async fn put_block(&self, mut block: Block, l1_header: HeaderId) -> Result<()> { { - let expected_clock_tx = NSSATransaction::clock_invocation(block.header.timestamp); - - // Validate block structure: the last transaction must be the sole clock invocation. - let last_tx = - block.body.transactions.last().ok_or_else(|| { - anyhow::anyhow!("Block must contain at least one transaction") - })?; - anyhow::ensure!( - last_tx == &expected_clock_tx, - "Last transaction in block must be the canonical clock invocation" - ); - - let clock_count = block - .body - .transactions - .iter() - .filter(|tx| tx.is_invocation_of_clock_program(&[])) - .count(); - anyhow::ensure!( - clock_count == 1, - "Block must contain exactly one Block Context Program invocation" - ); - let mut state_guard = self.current_state.write().await; for transaction in &block.body.transactions { diff --git a/nssa/core/src/nullifier.rs b/nssa/core/src/nullifier.rs index bb11cb4b..0e15ec74 100644 --- a/nssa/core/src/nullifier.rs +++ b/nssa/core/src/nullifier.rs @@ -55,7 +55,7 @@ pub type NullifierSecretKey = [u8; 32]; #[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[cfg_attr( any(feature = "host", test), - derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash) + derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash) )] pub struct Nullifier(pub(super) [u8; 32]); diff --git a/nssa/src/lib.rs b/nssa/src/lib.rs index 723c5633..4911d792 100644 --- a/nssa/src/lib.rs +++ b/nssa/src/lib.rs @@ -20,6 +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 mod encoding; pub mod error; @@ -30,6 +31,7 @@ pub mod program_deployment_transaction; pub mod public_transaction; mod signature; mod state; +mod state_diff; pub mod program_methods { include!(concat!(env!("OUT_DIR"), "/program_methods/mod.rs")); diff --git a/nssa/src/privacy_preserving_transaction/transaction.rs b/nssa/src/privacy_preserving_transaction/transaction.rs index db2bd773..216427bc 100644 --- a/nssa/src/privacy_preserving_transaction/transaction.rs +++ b/nssa/src/privacy_preserving_transaction/transaction.rs @@ -1,6 +1,5 @@ use std::{ - collections::{HashMap, HashSet}, - hash::Hash, + collections::HashSet, hash::Hash, }; use borsh::{BorshDeserialize, BorshSerialize}; @@ -13,6 +12,7 @@ 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)] @@ -30,12 +30,12 @@ impl PrivacyPreservingTransaction { } } - pub(crate) fn validate_and_produce_public_state_diff( + pub fn validate_and_produce_public_state_diff( &self, state: &V03State, block_id: BlockId, timestamp: Timestamp, - ) -> Result, NssaError> { + ) -> Result { let message = &self.message; let witness_set = &self.witness_set; @@ -124,12 +124,24 @@ impl PrivacyPreservingTransaction { // 6. Nullifier uniqueness state.check_nullifiers_are_valid(&message.new_nullifiers)?; - Ok(message + let public_diff = message .public_account_ids .iter() .copied() .zip(message.public_post_states.clone()) - .collect()) + .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] diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index 0ce52c56..ef2ea8ea 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -14,6 +14,7 @@ use crate::{ error::NssaError, public_transaction::{Message, WitnessSet}, state::MAX_NUMBER_CHAINED_CALLS, + state_diff::ValidatedStateDiff, }; #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] @@ -68,12 +69,12 @@ impl PublicTransaction { hasher.finalize_fixed().into() } - pub(crate) fn validate_and_produce_public_state_diff( + pub fn validate_and_produce_public_state_diff( &self, state: &V03State, block_id: BlockId, timestamp: Timestamp, - ) -> Result, NssaError> { + ) -> Result { let message = self.message(); let witness_set = self.witness_set(); @@ -270,7 +271,7 @@ impl PublicTransaction { ); } - Ok(state_diff) + Ok(ValidatedStateDiff::new(signer_account_ids, state_diff, vec![], vec![])) } } diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 801707a7..d3d21cc1 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -19,6 +19,7 @@ use crate::{ privacy_preserving_transaction::PrivacyPreservingTransaction, program::Program, program_deployment_transaction::ProgramDeploymentTransaction, public_transaction::PublicTransaction, + state_diff::ValidatedStateDiff, }; pub const MAX_NUMBER_CHAINED_CALLS: usize = 10; @@ -79,7 +80,7 @@ impl NullifierSet { Self(BTreeSet::new()) } - fn extend(&mut self, new_nullifiers: Vec) { + fn extend(&mut self, new_nullifiers: &[Nullifier]) { self.0.extend(new_nullifiers); } @@ -184,29 +185,29 @@ impl V03State { self.programs.insert(program.id(), program); } + pub fn apply_state_diff(&mut self, diff: ValidatedStateDiff) { + #[expect( + clippy::iter_over_hash_type, + reason = "Iteration order doesn't matter here" + )] + for (account_id, account) in diff.public_diff { + *self.get_account_by_id_mut(account_id) = account; + } + for account_id in diff.signer_account_ids { + self.get_account_by_id_mut(account_id).nonce.public_account_nonce_increment(); + } + self.private_state.0.extend(&diff.new_commitments); + self.private_state.1.extend(&diff.new_nullifiers); + } + pub fn transition_from_public_transaction( &mut self, tx: &PublicTransaction, block_id: BlockId, timestamp: Timestamp, ) -> Result<(), NssaError> { - let state_diff = tx.validate_and_produce_public_state_diff(self, block_id, timestamp)?; - - #[expect( - clippy::iter_over_hash_type, - reason = "Iteration order doesn't matter here" - )] - for (account_id, post) in state_diff { - let current_account = self.get_account_by_id_mut(account_id); - - *current_account = post; - } - - for account_id in tx.signer_account_ids() { - let current_account = self.get_account_by_id_mut(account_id); - current_account.nonce.public_account_nonce_increment(); - } - + let diff = tx.validate_and_produce_public_state_diff(self, block_id, timestamp)?; + self.apply_state_diff(diff); Ok(()) } @@ -216,40 +217,8 @@ impl V03State { block_id: BlockId, timestamp: Timestamp, ) -> Result<(), NssaError> { - // 1. Verify the transaction satisfies acceptance criteria - let public_state_diff = - tx.validate_and_produce_public_state_diff(self, block_id, timestamp)?; - - let message = tx.message(); - - // 2. Add new commitments - self.private_state.0.extend(&message.new_commitments); - - // 3. Add new nullifiers - let new_nullifiers = message - .new_nullifiers - .iter() - .cloned() - .map(|(nullifier, _)| nullifier) - .collect::>(); - self.private_state.1.extend(new_nullifiers); - - // 4. Update public accounts - #[expect( - clippy::iter_over_hash_type, - reason = "Iteration order doesn't matter here" - )] - for (account_id, post) in public_state_diff { - let current_account = self.get_account_by_id_mut(account_id); - *current_account = post; - } - - // 5. Increment nonces for public signers - for account_id in tx.signer_account_ids() { - let current_account = self.get_account_by_id_mut(account_id); - current_account.nonce.public_account_nonce_increment(); - } - + let diff = tx.validate_and_produce_public_state_diff(self, block_id, timestamp)?; + self.apply_state_diff(diff); Ok(()) } diff --git a/sequencer/core/src/lib.rs b/sequencer/core/src/lib.rs index 01f8ab3a..09f9603e 100644 --- a/sequencer/core/src/lib.rs +++ b/sequencer/core/src/lib.rs @@ -15,7 +15,7 @@ use logos_blockchain_key_management_system_service::keys::{ED25519_SECRET_KEY_SI use mempool::{MemPool, MemPoolHandle}; #[cfg(feature = "mock")] pub use mock::SequencerCoreWithMockClients; -use nssa::V03State; +use nssa::{V03State, ValidatedStateDiff}; use nssa_core::{BlockId, Timestamp}; pub use storage::error::DbError; use testnet_initial_state::initial_state; @@ -204,6 +204,23 @@ impl SequencerCore Option> { + 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, + } + } + /// Produces new block from transactions in mempool and packs it into a `SignedMantleTx`. pub fn produce_new_block_with_mempool_transactions( &mut self, @@ -235,16 +252,27 @@ impl SequencerCore { + let touches_system = clock_accounts_pre + .iter() + .any(|(id, pre)| diff.public_diff().get(id).is_some_and(|post| post != pre)); + if touches_system { + warn!( + "Dropping transaction from mempool: user transactions may not modify the system clock account" + ); + continue; + } + Some(diff) + } + Some(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 let temp_valid_transactions = @@ -271,23 +299,25 @@ impl SequencerCore { - valid_transactions.push(valid_tx); - - info!("Validated transaction with hash {tx_hash}, including it in block"); - - if valid_transactions.len() >= self.sequencer_config.max_num_tx_in_block { - 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; + } } } - Err(err) => { - error!( - "Transaction with hash {tx_hash} failed execution check with error: {err:#?}, skipping it", - ); - // TODO: Probably need to handle unsuccessful transaction execution? - } + } + + valid_transactions.push(tx); + info!("Validated transaction with hash {tx_hash}, including it in block"); + if valid_transactions.len() >= self.sequencer_config.max_num_tx_in_block { + break; } }