refactor so that indexer checks clock constraints

This commit is contained in:
Sergio Chouhy 2026-04-02 18:24:11 -03:00
parent 6a467da3b1
commit b525447e2d
30 changed files with 141 additions and 90 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,6 +1,6 @@
use borsh::{BorshDeserialize, BorshSerialize};
use log::warn;
use nssa::{AccountId, V03State};
use nssa::{AccountId, V03State, ValidatedStateDiff};
use nssa_core::{BlockId, Timestamp};
use serde::{Deserialize, Serialize};
@ -83,21 +83,53 @@ impl NSSATransaction {
}
}
/// Validates the transaction against the current state and returns the resulting diff
/// without applying it. Rejects transactions that modify clock system accounts.
pub fn validate_on_state(
&self,
state: &V03State,
block_id: BlockId,
timestamp: Timestamp,
) -> Result<ValidatedStateDiff, nssa::error::NssaError> {
let diff = match self {
Self::Public(tx) => {
ValidatedStateDiff::from_public_transaction(tx, state, block_id, timestamp)
}
Self::PrivacyPreserving(tx) => ValidatedStateDiff::from_privacy_preserving_transaction(
tx, state, block_id, timestamp,
),
Self::ProgramDeployment(tx) => {
ValidatedStateDiff::from_program_deployment_transaction(tx, state)
}
}?;
let public_diff = diff.public_diff();
let touches_clock = nssa::CLOCK_PROGRAM_ACCOUNT_IDS.iter().any(|id| {
public_diff
.get(id)
.is_some_and(|post| *post != state.get_account_by_id(*id))
});
if touches_clock {
return Err(nssa::error::NssaError::InvalidInput(
"Transaction modifies system clock accounts".into(),
));
}
Ok(diff)
}
/// Validates the transaction against the current state, rejects modifications to clock
/// system accounts, and applies the resulting diff to the state.
pub fn execute_check_on_state(
self,
state: &mut V03State,
block_id: BlockId,
timestamp: Timestamp,
) -> Result<Self, nssa::error::NssaError> {
match &self {
Self::Public(tx) => state.transition_from_public_transaction(tx, block_id, timestamp),
Self::PrivacyPreserving(tx) => {
state.transition_from_privacy_preserving_transaction(tx, block_id, timestamp)
}
Self::ProgramDeployment(tx) => state.transition_from_program_deployment_transaction(tx),
}
.inspect_err(|err| warn!("Error at transition {err:#?}"))?;
let diff = self
.validate_on_state(state, block_id, timestamp)
.inspect_err(|err| warn!("Error at transition {err:#?}"))?;
state.apply_state_diff(diff);
Ok(self)
}
}

View File

@ -122,7 +122,18 @@ impl IndexerStore {
{
let mut state_guard = self.current_state.write().await;
for transaction in &block.body.transactions {
let (clock_tx, user_txs) = block
.body
.transactions
.split_last()
.ok_or_else(|| anyhow::anyhow!("Block has no transactions"))?;
anyhow::ensure!(
*clock_tx == NSSATransaction::clock_invocation(block.header.timestamp),
"Last transaction in block must be the clock invocation for the block timestamp"
);
for transaction in user_txs {
transaction
.clone()
.transaction_stateless_check()?
@ -132,6 +143,16 @@ impl IndexerStore {
block.header.timestamp,
)?;
}
// Apply the clock invocation directly (it is expected to modify clock accounts).
let NSSATransaction::Public(clock_public_tx) = clock_tx else {
anyhow::bail!("Clock invocation must be a public transaction");
};
state_guard.transition_from_public_transaction(
clock_public_tx,
block.header.block_id,
block.header.timestamp,
)?;
}
// ToDo: Currently we are fetching only finalized blocks

View File

@ -0,0 +1,11 @@
[package]
name = "clock_core"
version = "0.1.0"
edition = "2024"
license = { workspace = true }
[lints]
workspace = true
[dependencies]
nssa_core.workspace = true

View File

@ -0,0 +1,49 @@
//! Core data structures and constants for the Clock Program.
use nssa_core::{Timestamp, account::AccountId};
/// The instruction type for the Clock Program. The sequencer passes the current block timestamp.
pub type Instruction = Timestamp;
pub const CLOCK_01_PROGRAM_ACCOUNT_ID: AccountId =
AccountId::new(*b"/LEZ/ClockProgramAccount/0000001");
pub const CLOCK_10_PROGRAM_ACCOUNT_ID: AccountId =
AccountId::new(*b"/LEZ/ClockProgramAccount/0000010");
pub const CLOCK_50_PROGRAM_ACCOUNT_ID: AccountId =
AccountId::new(*b"/LEZ/ClockProgramAccount/0000050");
/// All clock program account ID int the order expected by the clock program
pub const CLOCK_PROGRAM_ACCOUNT_IDS: [AccountId; 3] = [
CLOCK_01_PROGRAM_ACCOUNT_ID,
CLOCK_10_PROGRAM_ACCOUNT_ID,
CLOCK_50_PROGRAM_ACCOUNT_ID,
];
/// The data stored in a clock account: `[block_id: u64 LE | timestamp: u64 LE]`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ClockAccountData {
pub block_id: u64,
pub timestamp: Timestamp,
}
impl ClockAccountData {
#[must_use]
pub fn to_bytes(self) -> [u8; 16] {
let mut data = [0_u8; 16];
data[..8].copy_from_slice(&self.block_id.to_le_bytes());
data[8..].copy_from_slice(&self.timestamp.to_le_bytes());
data
}
#[must_use]
pub fn from_bytes(bytes: &[u8; 16]) -> Self {
let block_id = u64::from_le_bytes(bytes[..8].try_into().unwrap());
let timestamp = u64::from_le_bytes(bytes[8..].try_into().unwrap());
Self {
block_id,
timestamp,
}
}
}

View File

@ -15,8 +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, ValidatedStateDiff};
use nssa_core::{BlockId, Timestamp};
use nssa::V03State;
pub use storage::error::DbError;
use testnet_initial_state::initial_state;
@ -164,28 +163,6 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
(sequencer_core, mempool_handle)
}
fn execute_check_transaction_on_state(
&mut self,
tx: NSSATransaction,
block_id: BlockId,
timestamp: Timestamp,
) -> Result<NSSATransaction, nssa::error::NssaError> {
match &tx {
NSSATransaction::Public(tx) => self
.state
.transition_from_public_transaction(tx, block_id, timestamp),
NSSATransaction::PrivacyPreserving(tx) => self
.state
.transition_from_privacy_preserving_transaction(tx, block_id, timestamp),
NSSATransaction::ProgramDeployment(tx) => self
.state
.transition_from_program_deployment_transaction(tx),
}
.inspect_err(|err| warn!("Error at transition {err:#?}"))?;
Ok(tx)
}
pub async fn produce_new_block(&mut self) -> Result<u64> {
let (tx, _msg_id) = self
.produce_new_block_with_mempool_transactions()
@ -204,30 +181,6 @@ 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,
) -> Result<ValidatedStateDiff, nssa::error::NssaError> {
match transaction {
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)
}
}
}
/// Produces new block from transactions in mempool and packs it into a `SignedMantleTx`.
pub fn produce_new_block_with_mempool_transactions(
&mut self,
@ -249,33 +202,15 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
let new_block_timestamp = u64::try_from(chrono::Utc::now().timestamp_millis())
.expect("Timestamp must be positive");
// Note: the clock accounts are only modified by the clock program, which is invoked
// exclusively by the sequencer as the mandatory last transaction in each block. All user
// transactions are processed before that invocation, so this snapshot is always current
// and constant for all transactions in the block
let clock_accounts_pre =
nssa::CLOCK_PROGRAM_ACCOUNT_IDS.map(|id| (id, self.state.get_account_by_id(id)));
while let Some(tx) = self.mempool.pop() {
let tx_hash = tx.hash();
let validated_diff = match self.validate_transaction_and_produce_state_diff(
&tx,
let validated_diff = match tx.validate_on_state(
&self.state,
new_block_height,
new_block_timestamp,
) {
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;
}
diff
}
Ok(diff) => diff,
Err(err) => {
error!(
"Transaction with hash {tx_hash} failed execution check with error: {err:#?}, skipping it",
@ -319,13 +254,17 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
}
// Append the Block Context Program invocation as the mandatory last transaction.
let clock_nssa_tx = self
.execute_check_transaction_on_state(
NSSATransaction::clock_invocation(new_block_timestamp),
let clock_nssa_tx = NSSATransaction::clock_invocation(new_block_timestamp);
self.state
.transition_from_public_transaction(
match &clock_nssa_tx {
NSSATransaction::Public(tx) => tx,
_ => unreachable!("clock_invocation always returns Public"),
},
new_block_height,
new_block_timestamp,
)
.context("Clock transaction failed \u{2014} aborting block production")?;
.context("Clock transaction failed. Aborting block production.")?;
valid_transactions.push(clock_nssa_tx);
let hashable_data = HashableBlockData {
@ -454,8 +393,6 @@ mod tests {
use common::{test_utils::sequencer_sign_key_for_testing, transaction::NSSATransaction};
use logos_blockchain_core::mantle::ops::channel::ChannelId;
use mempool::MemPoolHandle;
use testnet_initial_state::{initial_accounts, initial_pub_accounts_private_keys};
use crate::{
@ -582,7 +519,7 @@ mod tests {
let tx = tx.transaction_stateless_check().unwrap();
// Signature is not from sender. Execution fails
let result = sequencer.execute_check_transaction_on_state(tx, 0, 0);
let result = tx.execute_check_on_state(&mut sequencer.state, 0, 0);
assert!(matches!(
result,
@ -608,7 +545,9 @@ mod tests {
// Passed pre-check
assert!(result.is_ok());
let result = sequencer.execute_check_transaction_on_state(result.unwrap(), 0, 0);
let result = result
.unwrap()
.execute_check_on_state(&mut sequencer.state, 0, 0);
let is_failed_at_balance_mismatch = matches!(
result.err().unwrap(),
nssa::error::NssaError::ProgramExecutionFailed(_)
@ -630,8 +569,7 @@ mod tests {
acc1, 0, acc2, 100, &sign_key1,
);
sequencer
.execute_check_transaction_on_state(tx, 0, 0)
tx.execute_check_on_state(&mut sequencer.state, 0, 0)
.unwrap();
let bal_from = sequencer.state.get_account_by_id(acc1).balance;