mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-04-12 14:13:07 +00:00
add validated state diff
This commit is contained in:
parent
fa2fd857a9
commit
40c7b308a9
@ -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<Self, TransactionMalformationError> {
|
||||
// Stateless checks here
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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]);
|
||||
|
||||
|
||||
@ -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"));
|
||||
|
||||
@ -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<HashMap<AccountId, Account>, NssaError> {
|
||||
) -> Result<ValidatedStateDiff, NssaError> {
|
||||
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]
|
||||
|
||||
@ -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<HashMap<AccountId, Account>, NssaError> {
|
||||
) -> Result<ValidatedStateDiff, NssaError> {
|
||||
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![]))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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<Nullifier>) {
|
||||
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::<Vec<Nullifier>>();
|
||||
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(())
|
||||
}
|
||||
|
||||
|
||||
@ -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<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
|
||||
Ok(self.chain_height)
|
||||
}
|
||||
|
||||
fn validate_transaction_and_produce_state_diff(
|
||||
&self,
|
||||
transaction: &NSSATransaction,
|
||||
block_id: BlockId,
|
||||
timestamp: Timestamp,
|
||||
) -> Option<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,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
|
||||
while let Some(tx) = self.mempool.pop() {
|
||||
let tx_hash = tx.hash();
|
||||
|
||||
// The Block Context Program is system-only. Reject:
|
||||
// - any public tx that invokes the clock program, and
|
||||
// - any PP tx that declares a modified post-state for a clock account.
|
||||
let touches_system = tx.is_invocation_of_clock_program(&clock_accounts_pre);
|
||||
if touches_system {
|
||||
warn!(
|
||||
"Dropping transaction from mempool: user transactions may not modify the system clock account"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let validated_diff = match self.validate_transaction_and_produce_state_diff(&tx, new_block_height, new_block_timestamp) {
|
||||
Some(Ok(diff)) => {
|
||||
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<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
|
||||
break;
|
||||
}
|
||||
|
||||
match self.execute_check_transaction_on_state(tx, new_block_height, new_block_timestamp)
|
||||
{
|
||||
Ok(valid_tx) => {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user