minor refactor

This commit is contained in:
Sergio Chouhy 2026-04-01 00:01:11 -03:00
parent 3c5a1c9d0a
commit fa2fd857a9
11 changed files with 93 additions and 88 deletions

10
Cargo.lock generated
View File

@ -1462,6 +1462,13 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
[[package]]
name = "clock_core"
version = "0.1.0"
dependencies = [
"nssa_core",
]
[[package]] [[package]]
name = "cobs" name = "cobs"
version = "0.3.0" version = "0.3.0"
@ -1511,6 +1518,7 @@ dependencies = [
"anyhow", "anyhow",
"base64 0.22.1", "base64 0.22.1",
"borsh", "borsh",
"clock_core",
"hex", "hex",
"log", "log",
"logos-blockchain-common-http-client", "logos-blockchain-common-http-client",
@ -5259,6 +5267,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"borsh", "borsh",
"clock_core",
"env_logger", "env_logger",
"hex", "hex",
"hex-literal 1.1.0", "hex-literal 1.1.0",
@ -5897,6 +5906,7 @@ dependencies = [
"amm_program", "amm_program",
"ata_core", "ata_core",
"ata_program", "ata_program",
"clock_core",
"nssa_core", "nssa_core",
"risc0-zkvm", "risc0-zkvm",
"serde", "serde",

View File

@ -15,6 +15,7 @@ members = [
"nssa/core", "nssa/core",
"programs/amm/core", "programs/amm/core",
"programs/amm", "programs/amm",
"programs/clock/core",
"programs/token/core", "programs/token/core",
"programs/token", "programs/token",
"programs/associated_token_account/core", "programs/associated_token_account/core",
@ -56,6 +57,7 @@ indexer_service_protocol = { path = "indexer/service/protocol" }
indexer_service_rpc = { path = "indexer/service/rpc" } indexer_service_rpc = { path = "indexer/service/rpc" }
wallet = { path = "wallet" } wallet = { path = "wallet" }
wallet-ffi = { path = "wallet-ffi", default-features = false } wallet-ffi = { path = "wallet-ffi", default-features = false }
clock_core = { path = "programs/clock/core" }
token_core = { path = "programs/token/core" } token_core = { path = "programs/token/core" }
token_program = { path = "programs/token" } token_program = { path = "programs/token" }
amm_core = { path = "programs/amm/core" } amm_core = { path = "programs/amm/core" }

View File

@ -10,6 +10,7 @@ workspace = true
[dependencies] [dependencies]
nssa.workspace = true nssa.workspace = true
nssa_core.workspace = true nssa_core.workspace = true
clock_core.workspace = true
anyhow.workspace = true anyhow.workspace = true
thiserror.workspace = true thiserror.workspace = true

View File

@ -47,14 +47,10 @@ impl NSSATransaction {
/// Returns the canonical Block Context Program invocation transaction for the given block /// Returns the canonical Block Context Program invocation transaction for the given block
/// timestamp. Every valid block must end with exactly one occurrence of this transaction. /// timestamp. Every valid block must end with exactly one occurrence of this transaction.
#[must_use] #[must_use]
pub fn clock_invocation(timestamp: nssa_core::Timestamp) -> Self { pub fn clock_invocation(timestamp: clock_core::Instruction) -> Self {
let message = nssa::public_transaction::Message::try_new( let message = nssa::public_transaction::Message::try_new(
nssa::program::Program::clock().id(), nssa::program::Program::clock().id(),
vec![ clock_core::CLOCK_PROGRAM_ACCOUNT_IDS.to_vec(),
nssa::CLOCK_01_PROGRAM_ACCOUNT_ID,
nssa::CLOCK_10_PROGRAM_ACCOUNT_ID,
nssa::CLOCK_50_PROGRAM_ACCOUNT_ID,
],
vec![], vec![],
timestamp, timestamp,
) )
@ -65,6 +61,28 @@ 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 // TODO: Introduce type-safe wrapper around checked transaction, e.g. AuthenticatedTransaction
pub fn transaction_stateless_check(self) -> Result<Self, TransactionMalformationError> { pub fn transaction_stateless_check(self) -> Result<Self, TransactionMalformationError> {
// Stateless checks here // Stateless checks here

View File

@ -136,7 +136,7 @@ impl IndexerStore {
.body .body
.transactions .transactions
.iter() .iter()
.filter(|tx| *tx == &expected_clock_tx) .filter(|tx| tx.is_invocation_of_clock_program(&[]))
.count(); .count();
anyhow::ensure!( anyhow::ensure!(
clock_count == 1, clock_count == 1,

View File

@ -9,6 +9,7 @@ workspace = true
[dependencies] [dependencies]
nssa_core = { workspace = true, features = ["host"] } nssa_core = { workspace = true, features = ["host"] }
clock_core.workspace = true
anyhow.workspace = true anyhow.workspace = true
thiserror.workspace = true thiserror.workspace = true

View File

@ -17,7 +17,8 @@ pub use program_methods::PRIVACY_PRESERVING_CIRCUIT_ID;
pub use public_transaction::PublicTransaction; pub use public_transaction::PublicTransaction;
pub use signature::{PrivateKey, PublicKey, Signature}; pub use signature::{PrivateKey, PublicKey, Signature};
pub use state::{ pub use state::{
CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID, V03State, CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID,
CLOCK_PROGRAM_ACCOUNT_IDS, V03State,
}; };
pub mod encoding; pub mod encoding;

View File

@ -8,6 +8,12 @@ use nssa_core::{
program::ProgramId, program::ProgramId,
}; };
pub use clock_core::{
CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID,
CLOCK_PROGRAM_ACCOUNT_IDS,
};
use clock_core::ClockAccountData;
use crate::{ use crate::{
error::NssaError, merkle_tree::MerkleTree, error::NssaError, merkle_tree::MerkleTree,
privacy_preserving_transaction::PrivacyPreservingTransaction, program::Program, privacy_preserving_transaction::PrivacyPreservingTransaction, program::Program,
@ -17,15 +23,6 @@ use crate::{
pub const MAX_NUMBER_CHAINED_CALLS: usize = 10; pub const MAX_NUMBER_CHAINED_CALLS: usize = 10;
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");
#[derive(Clone, BorshSerialize, BorshDeserialize)] #[derive(Clone, BorshSerialize, BorshDeserialize)]
#[cfg_attr(test, derive(Debug, PartialEq, Eq))] #[cfg_attr(test, derive(Debug, PartialEq, Eq))]
pub struct CommitmentSet { pub struct CommitmentSet {
@ -166,14 +163,9 @@ impl V03State {
} }
fn insert_clock_accounts(&mut self, genesis_timestamp: nssa_core::Timestamp) { fn insert_clock_accounts(&mut self, genesis_timestamp: nssa_core::Timestamp) {
let mut data = [0_u8; 16]; let data = ClockAccountData { block_id: 0, timestamp: genesis_timestamp }.to_bytes();
data[8..].copy_from_slice(&genesis_timestamp.to_le_bytes());
let clock_program_id = Program::clock().id(); let clock_program_id = Program::clock().id();
for account_id in [ for account_id in CLOCK_PROGRAM_ACCOUNT_IDS {
CLOCK_01_PROGRAM_ACCOUNT_ID,
CLOCK_10_PROGRAM_ACCOUNT_ID,
CLOCK_50_PROGRAM_ACCOUNT_ID,
] {
self.public_state.insert( self.public_state.insert(
account_id, account_id,
Account { Account {
@ -400,7 +392,7 @@ pub mod tests {
signature::PrivateKey, signature::PrivateKey,
state::{ state::{
CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID, CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID,
MAX_NUMBER_CHAINED_CALLS, CLOCK_PROGRAM_ACCOUNT_IDS, MAX_NUMBER_CHAINED_CALLS,
}, },
}; };
@ -543,11 +535,7 @@ pub mod tests {
..Account::default() ..Account::default()
}, },
); );
for account_id in [ for account_id in CLOCK_PROGRAM_ACCOUNT_IDS {
CLOCK_01_PROGRAM_ACCOUNT_ID,
CLOCK_10_PROGRAM_ACCOUNT_ID,
CLOCK_50_PROGRAM_ACCOUNT_ID,
] {
this.insert( this.insert(
account_id, account_id,
Account { Account {
@ -736,11 +724,7 @@ pub mod tests {
fn clock_transaction(timestamp: nssa_core::Timestamp) -> PublicTransaction { fn clock_transaction(timestamp: nssa_core::Timestamp) -> PublicTransaction {
let message = public_transaction::Message::try_new( let message = public_transaction::Message::try_new(
Program::clock().id(), Program::clock().id(),
vec![ CLOCK_PROGRAM_ACCOUNT_IDS.to_vec(),
CLOCK_01_PROGRAM_ACCOUNT_ID,
CLOCK_10_PROGRAM_ACCOUNT_ID,
CLOCK_50_PROGRAM_ACCOUNT_ID,
],
vec![], vec![],
timestamp, timestamp,
) )
@ -753,9 +737,8 @@ pub mod tests {
fn clock_account_data(state: &V03State, account_id: AccountId) -> (u64, nssa_core::Timestamp) { fn clock_account_data(state: &V03State, account_id: AccountId) -> (u64, nssa_core::Timestamp) {
let data = state.get_account_by_id(account_id).data.into_inner(); let data = state.get_account_by_id(account_id).data.into_inner();
let block_id = u64::from_le_bytes(data[..8].try_into().unwrap()); let parsed = clock_core::ClockAccountData::from_bytes(data[..16].try_into().unwrap());
let timestamp = u64::from_le_bytes(data[8..].try_into().unwrap()); (parsed.block_id, parsed.timestamp)
(block_id, timestamp)
} }
#[test] #[test]

View File

@ -9,6 +9,7 @@ workspace = true
[dependencies] [dependencies]
nssa_core.workspace = true nssa_core.workspace = true
clock_core.workspace = true
token_core.workspace = true token_core.workspace = true
token_program.workspace = true token_program.workspace = true
amm_core.workspace = true amm_core.workspace = true

View File

@ -1,10 +1,12 @@
use clock_core::{
CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID,
ClockAccountData, Instruction,
};
use nssa_core::{ use nssa_core::{
account::AccountWithMetadata, account::AccountWithMetadata,
program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs}, program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs},
}; };
type Instruction = nssa_core::Timestamp;
fn update_if_multiple( fn update_if_multiple(
pre: AccountWithMetadata, pre: AccountWithMetadata,
divisor: u64, divisor: u64,
@ -34,24 +36,28 @@ fn main() {
) = read_nssa_inputs::<Instruction>(); ) = read_nssa_inputs::<Instruction>();
let Ok([pre_01, pre_10, pre_50]) = <[_; 3]>::try_from(pre_states) else { let Ok([pre_01, pre_10, pre_50]) = <[_; 3]>::try_from(pre_states) else {
return; panic!("Invalid number of input accounts");
}; };
let prev_block_id = u64::from_le_bytes( // Verify pre-states correspond to the expected clock account IDs.
pre_01.account.data.clone().into_inner()[..8] if pre_01.account_id != CLOCK_01_PROGRAM_ACCOUNT_ID
|| pre_10.account_id != CLOCK_10_PROGRAM_ACCOUNT_ID
|| pre_50.account_id != CLOCK_50_PROGRAM_ACCOUNT_ID
{
panic!("Invalid input accounts");
}
let prev_data = ClockAccountData::from_bytes(
pre_01.account.data.clone().into_inner()[..16]
.try_into() .try_into()
.expect("Clock account data should contain a LE-encoded block_id u64"), .expect("Clock account data should be 16 bytes"),
); );
let current_block_id = prev_block_id let current_block_id = prev_data
.block_id
.checked_add(1) .checked_add(1)
.expect("Next block id should be within u64 boundaries"); .expect("Next block id should be within u64 boundaries");
let updated_data = { let updated_data = ClockAccountData { block_id: current_block_id, timestamp }.to_bytes();
let mut data = [0_u8; 16];
data[..8].copy_from_slice(&current_block_id.to_le_bytes());
data[8..].copy_from_slice(&timestamp.to_le_bytes());
data
};
let (pre_01, post_01) = update_if_multiple(pre_01, 1, current_block_id, updated_data); let (pre_01, post_01) = update_if_multiple(pre_01, 1, current_block_id, updated_data);
let (pre_10, post_10) = update_if_multiple(pre_10, 10, current_block_id, updated_data); let (pre_10, post_10) = update_if_multiple(pre_10, 10, current_block_id, updated_data);
@ -61,5 +67,6 @@ fn main() {
instruction_words, instruction_words,
vec![pre_01, pre_10, pre_50], vec![pre_01, pre_10, pre_50],
vec![post_01, post_10, post_50], vec![post_01, post_10, post_50],
).write(); )
.write();
} }

View File

@ -225,38 +225,20 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
let new_block_timestamp = u64::try_from(chrono::Utc::now().timestamp_millis()) let new_block_timestamp = u64::try_from(chrono::Utc::now().timestamp_millis())
.expect("Timestamp must be positive"); .expect("Timestamp must be positive");
let clock_program_id = nssa::program::Program::clock().id(); // Note: the clock accounts are only modified by the clock program, which is invoked
let clock_accounts_pre = [ // 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
nssa::CLOCK_01_PROGRAM_ACCOUNT_ID, // and constant for all transactions in the block
self.state let clock_accounts_pre =
.get_account_by_id(nssa::CLOCK_01_PROGRAM_ACCOUNT_ID), nssa::CLOCK_PROGRAM_ACCOUNT_IDS.map(|id| (id, self.state.get_account_by_id(id)));
),
(
nssa::CLOCK_10_PROGRAM_ACCOUNT_ID,
self.state
.get_account_by_id(nssa::CLOCK_10_PROGRAM_ACCOUNT_ID),
),
(
nssa::CLOCK_50_PROGRAM_ACCOUNT_ID,
self.state
.get_account_by_id(nssa::CLOCK_50_PROGRAM_ACCOUNT_ID),
),
];
while let Some(tx) = self.mempool.pop() { while let Some(tx) = self.mempool.pop() {
let tx_hash = tx.hash(); let tx_hash = tx.hash();
// The Block Context Program is system-only. Reject: // The Block Context Program is system-only. Reject:
// - any public tx that invokes the clock program ID, and // - any public tx that invokes the clock program, and
// - any PP tx that declares a modified post-state for the clock account. // - any PP tx that declares a modified post-state for a clock account.
let touches_system = match &tx { let touches_system = tx.is_invocation_of_clock_program(&clock_accounts_pre);
NSSATransaction::Public(p) => p.message().program_id == clock_program_id,
NSSATransaction::PrivacyPreserving(pp) => clock_accounts_pre
.iter()
.any(|(id, pre)| pp.public_post_state_for(id).is_some_and(|post| post != pre)),
NSSATransaction::ProgramDeployment(_) => false,
};
if touches_system { if touches_system {
warn!( warn!(
"Dropping transaction from mempool: user transactions may not modify the system clock account" "Dropping transaction from mempool: user transactions may not modify the system clock account"
@ -310,15 +292,14 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
} }
// Append the Block Context Program invocation as the mandatory last transaction. // Append the Block Context Program invocation as the mandatory last transaction.
match self.execute_check_transaction_on_state(NSSATransaction::clock_invocation(new_block_timestamp), new_block_height, new_block_timestamp) let clock_nssa_tx = self
{ .execute_check_transaction_on_state(
Ok(clock_nssa_tx) => { NSSATransaction::clock_invocation(new_block_timestamp),
valid_transactions.push(clock_nssa_tx); new_block_height,
} new_block_timestamp,
Err(err) => { )
error!("Clock transaction failed execution check: {err:#?}"); .context("Clock transaction failed — aborting block production")?;
} valid_transactions.push(clock_nssa_tx);
}
let hashable_data = HashableBlockData { let hashable_data = HashableBlockData {
block_id: new_block_height, block_id: new_block_height,