From 5c592312f9943cd63e781dd13213d3e2f52fb8b2 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 --- Cargo.lock | 30 +--- common/src/transaction.rs | 8 +- .../src/pages/transaction_page.rs | 19 ++ 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 | 80 ++++++++- .../transaction.rs | 5 +- nssa/src/public_transaction/transaction.rs | 15 +- nssa/src/state.rs | 165 +++++++++++++----- .../src/bin/privacy_preserving_circuit.rs | 17 +- programs/amm/src/tests.rs | 14 +- sequencer/core/src/lib.rs | 11 +- .../guest/src/bin/validity_window.rs | 12 +- 15 files changed, 288 insertions(+), 103 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c3b8c5f8..33f7d921 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1019,19 +1019,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" -[[package]] -name = "bitcoin-io" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" - [[package]] name = "bitcoin_hashes" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" dependencies = [ - "bitcoin-io", "hex-conservative", ] @@ -3977,7 +3970,6 @@ dependencies = [ "nssa", "nssa_core", "rand 0.8.5", - "secp256k1", "serde", "sha2", "thiserror 2.0.18", @@ -5269,13 +5261,13 @@ dependencies = [ "env_logger", "hex", "hex-literal 1.1.0", + "k256", "log", "nssa_core", "rand 0.8.5", "risc0-binfmt", "risc0-build", "risc0-zkvm", - "secp256k1", "serde", "serde_with", "sha2", @@ -7086,26 +7078,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "secp256k1" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c3c81b43dc2d8877c216a3fccf76677ee1ebccd429566d3e67447290d0c42b2" -dependencies = [ - "bitcoin_hashes", - "rand 0.9.2", - "secp256k1-sys", -] - -[[package]] -name = "secp256k1-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb913707158fadaf0d8702c2db0e857de66eb003ccfdda5924b5f5ac98efb38" -dependencies = [ - "cc", -] - [[package]] name = "security-framework" version = "3.7.0" 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 ed3d8aac..7fa7da6b 100644 --- a/explorer_service/src/pages/transaction_page.rs +++ b/explorer_service/src/pages/transaction_page.rs @@ -183,6 +183,25 @@ pub fn TransactionPage() -> impl IntoView { signatures_and_public_keys: _, proof, } = witness_set; + 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()); view! { 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 2777b512..a46bf023 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: validity_window.into(), + 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 0d8a7e14..2360b24b 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), +); impl Display for ValidityWindow { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 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 e88534d0..9aefb0ff 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -172,6 +172,8 @@ impl AccountPostState { } pub type BlockId = u64; +/// Unix timestamp in milliseconds. +pub type Timestamp = u64; #[derive(Clone, Copy, Serialize, Deserialize)] #[cfg_attr( @@ -181,6 +183,8 @@ pub type BlockId = u64; pub struct ValidityWindow { from: Option, to: Option, + from_timestamp: Option, + to_timestamp: Option, } impl ValidityWindow { @@ -190,11 +194,24 @@ 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). + /// A `None` bound on either side is treated as unbounded in that direction. + #[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) + } + /// Returns `true` if `id` falls within the half-open range `[from, to)`. /// A `None` bound on either side is treated as unbounded in that direction. + /// 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) @@ -205,10 +222,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(()) } /// Inclusive lower bound. `None` means the window starts at the beginning of the chain. @@ -222,6 +243,16 @@ impl ValidityWindow { pub const fn end(&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 { @@ -231,6 +262,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) @@ -328,6 +390,18 @@ impl ProgramOutput { self.validity_window = window.try_into()?; 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 75c15ddc..bfacf7b7 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, Claim, DEFAULT_PROGRAM_ID, validate_execution}, + program::{BlockId, ChainedCall, Claim, 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(); @@ -197,7 +198,7 @@ impl PublicTransaction { ensure!( program_output .validity_window - .is_valid_for_block_id(block_id), + .is_valid_for(block_id, timestamp_ms), NssaError::OutOfValidityWindow ); @@ -388,7 +389,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(_)))); } @@ -408,7 +409,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(_)))); } @@ -429,7 +430,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(_)))); } @@ -449,7 +450,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(_)))); } @@ -465,7 +466,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 4e7f4e9d..deef5c44 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::{ @@ -159,8 +159,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, @@ -184,9 +185,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(); @@ -341,7 +343,7 @@ pub mod tests { Commitment, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, encryption::{EphemeralPublicKey, Scalar, ViewingPublicKey}, - program::{BlockId, PdaSeed, ProgramId, ValidityWindow}, + program::{BlockId, PdaSeed, ProgramId, Timestamp, ValidityWindow}, }; use crate::{ @@ -576,7 +578,7 @@ pub mod tests { let balance_to_move = 5; let tx = transfer_transaction(from, &key, 0, to, &to_key, 0, 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); @@ -598,7 +600,7 @@ pub mod tests { assert!(state.get_account_by_id(from).balance < balance_to_move); let tx = transfer_transaction(from, &from_key, 0, to, &to_key, 0, 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); @@ -623,7 +625,7 @@ pub mod tests { let balance_to_move = 8; let tx = transfer_transaction(from, &from_key, 0, to, &to_key, 0, 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); @@ -652,7 +654,7 @@ pub mod tests { 0, 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, @@ -663,7 +665,7 @@ pub mod tests { 0, 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); @@ -685,7 +687,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))); } @@ -702,7 +704,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))); } @@ -719,7 +721,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))); } @@ -743,7 +745,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))); } @@ -767,7 +769,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))); } @@ -791,7 +793,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))); } @@ -815,7 +817,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))); } @@ -868,7 +870,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))); } @@ -886,7 +888,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))); } @@ -915,7 +917,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))); } @@ -1108,7 +1110,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()); @@ -1178,7 +1180,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); @@ -1242,7 +1244,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()); @@ -2170,7 +2172,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 { @@ -2188,7 +2190,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 { @@ -2266,7 +2268,7 @@ pub mod tests { public_transaction::WitnessSet::for_message(&message, &[&from_key, &to_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); @@ -2361,7 +2363,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); @@ -2401,7 +2403,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) @@ -2442,7 +2444,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); @@ -2499,7 +2501,7 @@ pub mod tests { public_transaction::WitnessSet::for_message(&message, &[&from_key, &to_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); @@ -2690,7 +2692,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 @@ -2706,6 +2708,85 @@ pub mod tests { ); } + #[test] + fn pda_mechanism_with_pinata_token_program() { + let pinata_token = Program::pinata_token(); + let token = Program::token(); + + let pinata_definition_id = AccountId::new([1; 32]); + let pinata_token_definition_id = AccountId::new([2; 32]); + // Total supply of pinata token will be in an account under a PDA. + let pinata_token_holding_id = AccountId::from((&pinata_token.id(), &PdaSeed::new([0; 32]))); + let winner_token_holding_id = AccountId::new([3; 32]); + + let expected_winner_account_holding = token_core::TokenHolding::Fungible { + definition_id: pinata_token_definition_id, + balance: 150, + }; + let expected_winner_token_holding_post = Account { + program_owner: token.id(), + data: Data::from(&expected_winner_account_holding), + ..Account::default() + }; + + let mut state = V03State::new_with_genesis_accounts(&[], &[]); + state.add_pinata_token_program(pinata_definition_id); + + // Execution of the token program to create new token for the pinata token + // definition and supply accounts + let total_supply: u128 = 10_000_000; + let instruction = token_core::Instruction::NewFungibleDefinition { + name: String::from("PINATA"), + total_supply, + }; + let message = public_transaction::Message::try_new( + token.id(), + vec![pinata_token_definition_id, pinata_token_holding_id], + vec![], + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); + + // Execution of winner's token holding account initialization + let instruction = token_core::Instruction::InitializeAccount; + let message = public_transaction::Message::try_new( + token.id(), + vec![pinata_token_definition_id, winner_token_holding_id], + vec![], + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + 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; + let message = public_transaction::Message::try_new( + pinata_token.id(), + vec![ + pinata_definition_id, + pinata_token_holding_id, + winner_token_holding_id, + ], + vec![], + solution, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + 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!( + winner_token_holding_post, + expected_winner_token_holding_post + ); + } + #[test] fn claiming_mechanism_cannot_claim_initialied_accounts() { let claimer = Program::claimer(); @@ -2727,7 +2808,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))); } @@ -2773,7 +2854,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); @@ -2842,7 +2923,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()); @@ -2942,7 +3023,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() ); @@ -2991,7 +3072,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()); @@ -3016,7 +3097,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))); @@ -3154,17 +3235,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.start(), validity_window.end()) { (Some(s), Some(e)) => s <= block_id && block_id < e, (Some(s), None) => s <= block_id, @@ -3205,9 +3287,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![], @@ -3227,7 +3310,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.start(), validity_window.end()) { (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 c561d139..fc866cf5 100644 --- a/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -38,10 +38,21 @@ impl ExecutionState { .iter() .filter_map(|output| output.validity_window.end()) .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 f0d53c96..14638f9d 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()); @@ -2814,7 +2814,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()); @@ -2898,7 +2898,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()); @@ -2971,7 +2971,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()); @@ -3033,7 +3033,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()); @@ -3090,7 +3090,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()); @@ -3140,7 +3140,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 545c63fa..1155754b 100644 --- a/sequencer/core/src/lib.rs +++ b/sequencer/core/src/lib.rs @@ -162,17 +162,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/test_program_methods/guest/src/bin/validity_window.rs b/test_program_methods/guest/src/bin/validity_window.rs index 00e8e5e8..8af01cd1 100644 --- a/test_program_methods/guest/src/bin/validity_window.rs +++ b/test_program_methods/guest/src/bin/validity_window.rs @@ -1,8 +1,13 @@ use nssa_core::program::{ - AccountPostState, ProgramInput, ProgramOutput, ValidityWindow, read_nssa_inputs, + AccountPostState, BlockId, ProgramInput, ProgramOutput, Timestamp, read_nssa_inputs, }; -type Instruction = ValidityWindow; +type Instruction = ( + Option, + Option, + Option, + Option, +); fn main() { let ( @@ -24,6 +29,7 @@ fn main() { vec![pre], vec![AccountPostState::new(post)], ) - .with_validity_window(validity_window) + .try_with_validity_window(validity_window) + .unwrap() .write(); }