From 0043f29dc211592f3c019f4c1bb05c899536fcc3 Mon Sep 17 00:00:00 2001 From: moudyellaz Date: Tue, 24 Mar 2026 10:24:05 +0100 Subject: [PATCH] feat: extend ValidityWindow with Unix timestamp bounds --- common/src/transaction.rs | 8 +- .../src/pages/transaction_page.rs | 23 ++++- indexer/core/src/block_store.rs | 2 +- indexer/service/protocol/src/convert.rs | 7 +- indexer/service/protocol/src/lib.rs | 4 +- indexer/service/src/mock_service.rs | 2 +- nssa/core/src/program.rs | 82 ++++++++++++++++- .../transaction.rs | 5 +- nssa/src/public_transaction/transaction.rs | 15 +-- nssa/src/state.rs | 92 ++++++++++--------- .../src/bin/privacy_preserving_circuit.rs | 17 +++- programs/amm/src/tests.rs | 14 +-- sequencer/core/src/lib.rs | 11 ++- storage/src/indexer.rs | 2 +- .../guest/src/bin/validity_window.rs | 15 ++- 15 files changed, 215 insertions(+), 84 deletions(-) diff --git a/common/src/transaction.rs b/common/src/transaction.rs index 1862dcc8..b69b3017 100644 --- a/common/src/transaction.rs +++ b/common/src/transaction.rs @@ -4,6 +4,7 @@ use nssa::{AccountId, V03State}; use serde::{Deserialize, Serialize}; use crate::{HashType, block::BlockId}; +use nssa_core::program::Timestamp; #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub enum NSSATransaction { @@ -69,11 +70,14 @@ impl NSSATransaction { self, state: &mut V03State, block_id: BlockId, + timestamp_ms: Timestamp, ) -> Result { match &self { - Self::Public(tx) => state.transition_from_public_transaction(tx, block_id), + Self::Public(tx) => { + state.transition_from_public_transaction(tx, block_id, timestamp_ms) + } Self::PrivacyPreserving(tx) => { - state.transition_from_privacy_preserving_transaction(tx, block_id) + state.transition_from_privacy_preserving_transaction(tx, block_id, timestamp_ms) } Self::ProgramDeployment(tx) => state.transition_from_program_deployment_transaction(tx), } diff --git a/explorer_service/src/pages/transaction_page.rs b/explorer_service/src/pages/transaction_page.rs index b549b1f8..a293b840 100644 --- a/explorer_service/src/pages/transaction_page.rs +++ b/explorer_service/src/pages/transaction_page.rs @@ -183,11 +183,24 @@ pub fn TransactionPage() -> impl IntoView { signatures_and_public_keys: _, proof, } = witness_set; - let validity_window_formatted = match validity_window.0 { - (Some(start), Some(end)) => format!("from {start} to {end}"), - (Some(start), None) => format!("from {start}"), - (None, Some(end)) => format!("until {end}"), - (None, None) => "unbounded".to_owned(), + let (block_from, block_to, ts_from, ts_to) = validity_window.0; + let block_part = match (block_from, block_to) { + (Some(start), Some(end)) => format!("block {start}..{end}"), + (Some(start), None) => format!("block {start}.."), + (None, Some(end)) => format!("block ..{end}"), + (None, None) => String::new(), + }; + let ts_part = match (ts_from, ts_to) { + (Some(start), Some(end)) => format!("ts {start}..{end}"), + (Some(start), None) => format!("ts {start}.."), + (None, Some(end)) => format!("ts ..{end}"), + (None, None) => String::new(), + }; + let validity_window_formatted = match (block_part.is_empty(), ts_part.is_empty()) { + (true, true) => "unbounded".to_owned(), + (false, true) => block_part, + (true, false) => ts_part, + (false, false) => format!("{block_part}, {ts_part}"), }; let proof_len = proof.map_or(0, |p| p.0.len()); diff --git a/indexer/core/src/block_store.rs b/indexer/core/src/block_store.rs index e4534f76..60ae7fe8 100644 --- a/indexer/core/src/block_store.rs +++ b/indexer/core/src/block_store.rs @@ -125,7 +125,7 @@ impl IndexerStore { transaction .clone() .transaction_stateless_check()? - .execute_check_on_state(&mut state_guard, block.header.block_id)?; + .execute_check_on_state(&mut state_guard, block.header.block_id, block.header.timestamp)?; } } diff --git a/indexer/service/protocol/src/convert.rs b/indexer/service/protocol/src/convert.rs index ec85d7fb..7fcd32e2 100644 --- a/indexer/service/protocol/src/convert.rs +++ b/indexer/service/protocol/src/convert.rs @@ -302,7 +302,12 @@ impl From for PrivacyPre .into_iter() .map(|(n, d)| (n.into(), d.into())) .collect(), - validity_window: ValidityWindow((validity_window.from(), validity_window.to())), + validity_window: ValidityWindow(( + validity_window.from(), + validity_window.to(), + validity_window.from_timestamp(), + validity_window.to_timestamp(), + )), } } } diff --git a/indexer/service/protocol/src/lib.rs b/indexer/service/protocol/src/lib.rs index a8f6da2c..feffcc2e 100644 --- a/indexer/service/protocol/src/lib.rs +++ b/indexer/service/protocol/src/lib.rs @@ -302,7 +302,9 @@ pub struct Nullifier( ); #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] -pub struct ValidityWindow(pub (Option, Option)); +pub struct ValidityWindow( + pub (Option, Option, Option, Option), +); #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] pub struct CommitmentSetDigest( diff --git a/indexer/service/src/mock_service.rs b/indexer/service/src/mock_service.rs index c5891b41..39d46269 100644 --- a/indexer/service/src/mock_service.rs +++ b/indexer/service/src/mock_service.rs @@ -124,7 +124,7 @@ impl MockIndexerService { indexer_service_protocol::Nullifier([tx_idx as u8; 32]), CommitmentSetDigest([0xff; 32]), )], - validity_window: ValidityWindow((None, None)), + validity_window: ValidityWindow((None, None, None, None)), }, witness_set: WitnessSet { signatures_and_public_keys: vec![], diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 5cd46432..c533d2fd 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -1,6 +1,6 @@ use std::collections::HashSet; -#[cfg(feature = "host")] +#[cfg(any(feature = "host", test))] use borsh::{BorshDeserialize, BorshSerialize}; use risc0_zkvm::{DeserializeOwned, guest::env, serde::Deserializer}; use serde::{Deserialize, Serialize}; @@ -154,6 +154,8 @@ impl AccountPostState { } pub type BlockId = u64; +/// Unix timestamp in milliseconds. +pub type Timestamp = u64; #[derive(Serialize, Deserialize, Clone, Copy)] #[cfg_attr( @@ -163,6 +165,8 @@ pub type BlockId = u64; pub struct ValidityWindow { from: Option, to: Option, + from_timestamp: Option, + to_timestamp: Option, } impl ValidityWindow { @@ -171,10 +175,22 @@ impl ValidityWindow { Self { from: None, to: None, + from_timestamp: None, + to_timestamp: None, } } + /// Valid for block IDs in the range [from, to) and timestamps in [from_timestamp, to_timestamp). + #[must_use] + pub fn is_valid_for(&self, block_id: BlockId, timestamp_ms: Timestamp) -> bool { + self.from.is_none_or(|start| block_id >= start) + && self.to.is_none_or(|end| block_id < end) + && self.from_timestamp.is_none_or(|t| timestamp_ms >= t) + && self.to_timestamp.is_none_or(|t| timestamp_ms < t) + } + /// Valid for block IDs in the range [from, to), where `from` is included and `to` is excluded. + /// Ignores timestamp bounds. #[must_use] pub fn is_valid_for_block_id(&self, id: BlockId) -> bool { self.from.is_none_or(|start| id >= start) && self.to.is_none_or(|end| id < end) @@ -184,10 +200,14 @@ impl ValidityWindow { if let (Some(from_id), Some(until_id)) = (self.from, self.to) && from_id >= until_id { - Err(InvalidWindow) - } else { - Ok(()) + return Err(InvalidWindow); } + if let (Some(from_ts), Some(until_ts)) = (self.from_timestamp, self.to_timestamp) + && from_ts >= until_ts + { + return Err(InvalidWindow); + } + Ok(()) } #[must_use] @@ -199,7 +219,18 @@ impl ValidityWindow { pub const fn to(&self) -> Option { self.to } + + #[must_use] + pub const fn from_timestamp(&self) -> Option { + self.from_timestamp + } + + #[must_use] + pub const fn to_timestamp(&self) -> Option { + self.to_timestamp + } } + impl TryFrom<(Option, Option)> for ValidityWindow { type Error = InvalidWindow; @@ -207,6 +238,37 @@ impl TryFrom<(Option, Option)> for ValidityWindow { let this = Self { from: value.0, to: value.1, + from_timestamp: None, + to_timestamp: None, + }; + this.check_window()?; + Ok(this) + } +} + +impl + TryFrom<( + Option, + Option, + Option, + Option, + )> for ValidityWindow +{ + type Error = InvalidWindow; + + fn try_from( + value: ( + Option, + Option, + Option, + Option, + ), + ) -> Result { + let this = Self { + from: value.0, + to: value.1, + from_timestamp: value.2, + to_timestamp: value.3, }; this.check_window()?; Ok(this) @@ -271,6 +333,18 @@ impl ProgramOutput { self.validity_window.check_window()?; Ok(self) } + + pub fn valid_from_timestamp(mut self, ts: Option) -> Result { + self.validity_window.from_timestamp = ts; + self.validity_window.check_window()?; + Ok(self) + } + + pub fn valid_until_timestamp(mut self, ts: Option) -> Result { + self.validity_window.to_timestamp = ts; + self.validity_window.check_window()?; + Ok(self) + } } /// Representation of a number as `lo + hi * 2^128`. diff --git a/nssa/src/privacy_preserving_transaction/transaction.rs b/nssa/src/privacy_preserving_transaction/transaction.rs index b1c30109..8823e364 100644 --- a/nssa/src/privacy_preserving_transaction/transaction.rs +++ b/nssa/src/privacy_preserving_transaction/transaction.rs @@ -7,7 +7,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use nssa_core::{ Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput, account::{Account, AccountWithMetadata}, - program::{BlockId, ValidityWindow}, + program::{BlockId, Timestamp, ValidityWindow}, }; use sha2::{Digest as _, digest::FixedOutput as _}; @@ -37,6 +37,7 @@ impl PrivacyPreservingTransaction { &self, state: &V03State, block_id: BlockId, + timestamp_ms: Timestamp, ) -> Result, NssaError> { let message = &self.message; let witness_set = &self.witness_set; @@ -94,7 +95,7 @@ impl PrivacyPreservingTransaction { } // Verify validity window - if !message.validity_window.is_valid_for_block_id(block_id) { + if !message.validity_window.is_valid_for(block_id, timestamp_ms) { return Err(NssaError::OutOfValidityWindow); } diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index 8aaf039e..9f93805e 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -4,7 +4,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use log::debug; use nssa_core::{ account::{Account, AccountId, AccountWithMetadata}, - program::{BlockId, ChainedCall, DEFAULT_PROGRAM_ID, validate_execution}, + program::{BlockId, ChainedCall, DEFAULT_PROGRAM_ID, Timestamp, validate_execution}, }; use sha2::{Digest as _, digest::FixedOutput as _}; @@ -71,6 +71,7 @@ impl PublicTransaction { &self, state: &V03State, block_id: BlockId, + timestamp_ms: Timestamp, ) -> Result, NssaError> { let message = self.message(); let witness_set = self.witness_set(); @@ -195,7 +196,7 @@ impl PublicTransaction { ensure!( program_output .validity_window - .is_valid_for_block_id(block_id), + .is_valid_for(block_id, timestamp_ms), NssaError::OutOfValidityWindow ); @@ -368,7 +369,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); + let result = tx.validate_and_produce_public_state_diff(&state, 1, 0); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } @@ -388,7 +389,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); + let result = tx.validate_and_produce_public_state_diff(&state, 1, 0); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } @@ -409,7 +410,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); + let result = tx.validate_and_produce_public_state_diff(&state, 1, 0); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } @@ -429,7 +430,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); + let result = tx.validate_and_produce_public_state_diff(&state, 1, 0); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } @@ -445,7 +446,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); + let result = tx.validate_and_produce_public_state_diff(&state, 1, 0); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } } diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 7f2a0ec8..58c09c92 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -4,7 +4,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use nssa_core::{ Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, Nullifier, account::{Account, AccountId, Nonce}, - program::{BlockId, ProgramId}, + program::{BlockId, ProgramId, Timestamp}, }; use crate::{ @@ -158,8 +158,9 @@ impl V03State { &mut self, tx: &PublicTransaction, block_id: BlockId, + timestamp_ms: Timestamp, ) -> Result<(), NssaError> { - let state_diff = tx.validate_and_produce_public_state_diff(self, block_id)?; + let state_diff = tx.validate_and_produce_public_state_diff(self, block_id, timestamp_ms)?; #[expect( clippy::iter_over_hash_type, @@ -183,9 +184,10 @@ impl V03State { &mut self, tx: &PrivacyPreservingTransaction, block_id: BlockId, + timestamp_ms: Timestamp, ) -> Result<(), NssaError> { // 1. Verify the transaction satisfies acceptance criteria - let public_state_diff = tx.validate_and_produce_public_state_diff(self, block_id)?; + let public_state_diff = tx.validate_and_produce_public_state_diff(self, block_id, timestamp_ms)?; let message = tx.message(); @@ -340,7 +342,7 @@ pub mod tests { Commitment, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, encryption::{EphemeralPublicKey, Scalar, ViewingPublicKey}, - program::{BlockId, PdaSeed, ProgramId}, + program::{BlockId, PdaSeed, ProgramId, Timestamp}, }; use crate::{ @@ -570,7 +572,7 @@ pub mod tests { let balance_to_move = 5; let tx = transfer_transaction(from, &key, 0, to, balance_to_move); - state.transition_from_public_transaction(&tx, 1).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); assert_eq!(state.get_account_by_id(from).balance, 95); assert_eq!(state.get_account_by_id(to).balance, 5); @@ -591,7 +593,7 @@ pub mod tests { assert!(state.get_account_by_id(from).balance < balance_to_move); let tx = transfer_transaction(from, &from_key, 0, to, balance_to_move); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::ProgramExecutionFailed(_)))); assert_eq!(state.get_account_by_id(from).balance, 100); @@ -615,7 +617,7 @@ pub mod tests { let balance_to_move = 8; let tx = transfer_transaction(from, &from_key, 0, to, balance_to_move); - state.transition_from_public_transaction(&tx, 1).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); assert_eq!(state.get_account_by_id(from).balance, 192); assert_eq!(state.get_account_by_id(to).balance, 108); @@ -635,10 +637,10 @@ pub mod tests { let balance_to_move = 5; let tx = transfer_transaction(account_id1, &key1, 0, account_id2, balance_to_move); - state.transition_from_public_transaction(&tx, 1).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let balance_to_move = 3; let tx = transfer_transaction(account_id2, &key2, 0, account_id3, balance_to_move); - state.transition_from_public_transaction(&tx, 1).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); assert_eq!(state.get_account_by_id(account_id1).balance, 95); assert_eq!(state.get_account_by_id(account_id2).balance, 2); @@ -660,7 +662,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -677,7 +679,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -694,7 +696,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -718,7 +720,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -742,7 +744,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -766,7 +768,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -790,7 +792,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -818,7 +820,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -843,7 +845,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -861,7 +863,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -890,7 +892,7 @@ pub mod tests { .unwrap(); let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -1083,7 +1085,7 @@ pub mod tests { assert!(!state.private_state.0.contains(&expected_new_commitment)); state - .transition_from_privacy_preserving_transaction(&tx, 1) + .transition_from_privacy_preserving_transaction(&tx, 1, 0) .unwrap(); let sender_post = state.get_account_by_id(sender_keys.account_id()); @@ -1153,7 +1155,7 @@ pub mod tests { assert!(!state.private_state.1.contains(&expected_new_nullifier)); state - .transition_from_privacy_preserving_transaction(&tx, 1) + .transition_from_privacy_preserving_transaction(&tx, 1, 0) .unwrap(); assert_eq!(state.public_state, previous_public_state); @@ -1217,7 +1219,7 @@ pub mod tests { assert!(!state.private_state.1.contains(&expected_new_nullifier)); state - .transition_from_privacy_preserving_transaction(&tx, 1) + .transition_from_privacy_preserving_transaction(&tx, 1, 0) .unwrap(); let recipient_post = state.get_account_by_id(recipient_keys.account_id()); @@ -2145,7 +2147,7 @@ pub mod tests { ); state - .transition_from_privacy_preserving_transaction(&tx, 1) + .transition_from_privacy_preserving_transaction(&tx, 1, 0) .unwrap(); let sender_private_account = Account { @@ -2163,7 +2165,7 @@ pub mod tests { &state, ); - let result = state.transition_from_privacy_preserving_transaction(&tx, 1); + let result = state.transition_from_privacy_preserving_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); let NssaError::InvalidInput(error_message) = result.err().unwrap() else { @@ -2240,7 +2242,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let recipient_post = state.get_account_by_id(to); @@ -2283,7 +2285,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let from_post = state.get_account_by_id(from); let to_post = state.get_account_by_id(to); @@ -2323,7 +2325,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!( result, Err(NssaError::MaxChainedCallsDepthExceeded) @@ -2364,7 +2366,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let from_post = state.get_account_by_id(from); let to_post = state.get_account_by_id(to); @@ -2420,7 +2422,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let from_post = state.get_account_by_id(from); let to_post = state.get_account_by_id(to); @@ -2529,7 +2531,7 @@ pub mod tests { let transaction = PrivacyPreservingTransaction::new(message, witness_set); state - .transition_from_privacy_preserving_transaction(&transaction, 1) + .transition_from_privacy_preserving_transaction(&transaction, 1, 0) .unwrap(); // Assert @@ -2585,7 +2587,7 @@ pub mod tests { .unwrap(); let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); // Execution of winner's token holding account initialization let instruction = token_core::Instruction::InitializeAccount; @@ -2598,7 +2600,7 @@ pub mod tests { .unwrap(); let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); // Submit a solution to the pinata program to claim the prize let solution: u128 = 989_106; @@ -2615,7 +2617,7 @@ pub mod tests { .unwrap(); let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let winner_token_holding_post = state.get_account_by_id(winner_token_holding_id); assert_eq!( @@ -2645,7 +2647,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } @@ -2691,7 +2693,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[&sender_key]); let tx = PublicTransaction::new(message, witness_set); - let res = state.transition_from_public_transaction(&tx, 1); + let res = state.transition_from_public_transaction(&tx, 1, 0); assert!(matches!(res, Err(NssaError::InvalidProgramBehavior))); let sender_post = state.get_account_by_id(sender_id); @@ -2760,7 +2762,7 @@ pub mod tests { let witness_set = WitnessSet::for_message(&message, proof, &[]); let tx = PrivacyPreservingTransaction::new(message, witness_set); - let result = state.transition_from_privacy_preserving_transaction(&tx, 1); + let result = state.transition_from_privacy_preserving_transaction(&tx, 1, 0); assert!(result.is_ok()); let nullifier = Nullifier::for_account_initialization(&private_keys.npk()); @@ -2813,7 +2815,7 @@ pub mod tests { // Claim should succeed assert!( state - .transition_from_privacy_preserving_transaction(&tx, 1) + .transition_from_privacy_preserving_transaction(&tx, 1, 0) .is_ok() ); @@ -2862,7 +2864,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); // Should succeed - no changes made, no claim needed assert!(result.is_ok()); @@ -2887,7 +2889,7 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 1); + let result = state.transition_from_public_transaction(&tx, 1, 0); // Should fail - cannot modify data without claiming the account assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); @@ -3024,17 +3026,18 @@ pub mod tests { let account_ids = vec![pre.account_id]; let nonces = vec![]; let program_id = validity_window_program.id(); + let instruction = (validity_window.0, validity_window.1, None::, None::); let message = public_transaction::Message::try_new( program_id, account_ids, nonces, - validity_window, + instruction, ) .unwrap(); let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); PublicTransaction::new(message, witness_set) }; - let result = state.transition_from_public_transaction(&tx, block_id); + let result = state.transition_from_public_transaction(&tx, block_id, 0); let is_inside_validity_window = match validity_window { (Some(s), Some(e)) => s <= block_id && block_id < e, (Some(s), None) => s <= block_id, @@ -3074,9 +3077,10 @@ pub mod tests { let shared_secret = SharedSecretKey::new(&esk, &account_keys.vpk()); let epk = EphemeralPublicKey::from_scalar(esk); + let instruction = (validity_window.0, validity_window.1, None::, None::); let (output, proof) = circuit::execute_and_prove( vec![pre], - Program::serialize_instruction(validity_window).unwrap(), + Program::serialize_instruction(instruction).unwrap(), vec![2], vec![(account_keys.npk(), shared_secret)], vec![], @@ -3096,7 +3100,7 @@ pub mod tests { let witness_set = WitnessSet::for_message(&message, proof, &[]); PrivacyPreservingTransaction::new(message, witness_set) }; - let result = state.transition_from_privacy_preserving_transaction(&tx, block_id); + let result = state.transition_from_privacy_preserving_transaction(&tx, block_id, 0); let is_inside_validity_window = match validity_window { (Some(s), Some(e)) => s <= block_id && block_id < e, (Some(s), None) => s <= block_id, diff --git a/program_methods/guest/src/bin/privacy_preserving_circuit.rs b/program_methods/guest/src/bin/privacy_preserving_circuit.rs index 08872564..a674b5b0 100644 --- a/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -34,10 +34,21 @@ impl ExecutionState { .iter() .filter_map(|output| output.validity_window.to()) .min(); + let valid_from_ts = program_outputs + .iter() + .filter_map(|output| output.validity_window.from_timestamp()) + .max(); + let valid_until_ts = program_outputs + .iter() + .filter_map(|output| output.validity_window.to_timestamp()) + .min(); - let validity_window = (valid_from_id, valid_until_id).try_into().expect( - "There should be non empty intersection in the program output validity windows", - ); + let validity_window = + (valid_from_id, valid_until_id, valid_from_ts, valid_until_ts) + .try_into() + .expect( + "There should be non empty intersection in the program output validity windows", + ); let mut execution_state = Self { pre_states: Vec::new(), diff --git a/programs/amm/src/tests.rs b/programs/amm/src/tests.rs index 86fdb4ff..3c870acd 100644 --- a/programs/amm/src/tests.rs +++ b/programs/amm/src/tests.rs @@ -2733,7 +2733,7 @@ fn simple_amm_remove() { ); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id()); let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id()); @@ -2813,7 +2813,7 @@ fn simple_amm_new_definition_inactive_initialized_pool_and_uninit_user_lp() { ); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id()); let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id()); @@ -2897,7 +2897,7 @@ fn simple_amm_new_definition_inactive_initialized_pool_init_user_lp() { ); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id()); let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id()); @@ -2969,7 +2969,7 @@ fn simple_amm_new_definition_uninitialized_pool() { ); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id()); let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id()); @@ -3031,7 +3031,7 @@ fn simple_amm_add() { ); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id()); let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id()); @@ -3088,7 +3088,7 @@ fn simple_amm_swap_1() { ); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id()); let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id()); @@ -3138,7 +3138,7 @@ fn simple_amm_swap_2() { ); let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1).unwrap(); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id()); let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id()); diff --git a/sequencer/core/src/lib.rs b/sequencer/core/src/lib.rs index cd20467b..90535adc 100644 --- a/sequencer/core/src/lib.rs +++ b/sequencer/core/src/lib.rs @@ -142,17 +142,24 @@ impl SequencerCore nssa_core::program::Timestamp { + u64::try_from(chrono::Utc::now().timestamp_millis()) + .expect("Timestamp must be positive") + } + fn execute_check_transaction_on_state( &mut self, tx: NSSATransaction, ) -> Result { + let block_id = self.next_block_id(); + let timestamp_ms = self.next_block_timestamp_ms(); match &tx { NSSATransaction::Public(tx) => self .state - .transition_from_public_transaction(tx, self.next_block_id()), + .transition_from_public_transaction(tx, block_id, timestamp_ms), NSSATransaction::PrivacyPreserving(tx) => self .state - .transition_from_privacy_preserving_transaction(tx, self.next_block_id()), + .transition_from_privacy_preserving_transaction(tx, block_id, timestamp_ms), NSSATransaction::ProgramDeployment(tx) => self .state .transition_from_program_deployment_transaction(tx), diff --git a/storage/src/indexer.rs b/storage/src/indexer.rs index 07ceba8a..a954cd29 100644 --- a/storage/src/indexer.rs +++ b/storage/src/indexer.rs @@ -677,7 +677,7 @@ impl RocksDBIO { "transaction pre check failed with err {err:?}" )) })? - .execute_check_on_state(&mut breakpoint, block.header.block_id) + .execute_check_on_state(&mut breakpoint, block.header.block_id, block.header.timestamp) .map_err(|err| { DbError::db_interaction_error(format!( "transaction execution failed with err {err:?}" diff --git a/test_program_methods/guest/src/bin/validity_window.rs b/test_program_methods/guest/src/bin/validity_window.rs index 03f31073..f049e06b 100644 --- a/test_program_methods/guest/src/bin/validity_window.rs +++ b/test_program_methods/guest/src/bin/validity_window.rs @@ -1,14 +1,19 @@ use nssa_core::program::{ - AccountPostState, BlockId, ProgramInput, ProgramOutput, read_nssa_inputs, + AccountPostState, BlockId, ProgramInput, ProgramOutput, Timestamp, read_nssa_inputs, }; -type Instruction = (Option, Option); +type Instruction = ( + Option, + Option, + Option, + Option, +); fn main() { let ( ProgramInput { pre_states, - instruction: (from_id, until_id), + instruction: (from_id, until_id, from_ts, until_ts), }, instruction_words, ) = read_nssa_inputs::(); @@ -27,6 +32,10 @@ fn main() { .valid_from_id(from_id) .unwrap() .valid_until_id(until_id) + .unwrap() + .valid_from_timestamp(from_ts) + .unwrap() + .valid_until_timestamp(until_ts) .unwrap(); output.write();