feat: extend ValidityWindow with Unix timestamp bounds

This commit is contained in:
moudyellaz 2026-03-24 10:24:05 +01:00 committed by Moudy
parent b8bb6913be
commit 5c592312f9
15 changed files with 288 additions and 103 deletions

30
Cargo.lock generated
View File

@ -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"

View File

@ -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<Self, nssa::error::NssaError> {
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),
}

View File

@ -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! {

View File

@ -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)?;
}
}

View File

@ -302,7 +302,12 @@ impl From<nssa::privacy_preserving_transaction::message::Message> 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(),
)),
}
}
}

View File

@ -302,7 +302,9 @@ pub struct Nullifier(
);
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub struct ValidityWindow(pub (Option<BlockId>, Option<BlockId>));
pub struct ValidityWindow(
pub (Option<BlockId>, Option<BlockId>, Option<TimeStamp>, Option<TimeStamp>),
);
impl Display for ValidityWindow {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {

View File

@ -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![],

View File

@ -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<BlockId>,
to: Option<BlockId>,
from_timestamp: Option<Timestamp>,
to_timestamp: Option<Timestamp>,
}
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<BlockId> {
self.to
}
#[must_use]
pub const fn from_timestamp(&self) -> Option<Timestamp> {
self.from_timestamp
}
#[must_use]
pub const fn to_timestamp(&self) -> Option<Timestamp> {
self.to_timestamp
}
}
impl TryFrom<(Option<BlockId>, Option<BlockId>)> for ValidityWindow {
@ -231,6 +262,37 @@ impl TryFrom<(Option<BlockId>, Option<BlockId>)> 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<BlockId>,
Option<BlockId>,
Option<Timestamp>,
Option<Timestamp>,
)> for ValidityWindow
{
type Error = InvalidWindow;
fn try_from(
value: (
Option<BlockId>,
Option<BlockId>,
Option<Timestamp>,
Option<Timestamp>,
),
) -> Result<Self, Self::Error> {
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<Timestamp>) -> Result<Self, InvalidWindow> {
self.validity_window.from_timestamp = ts;
self.validity_window.check_window()?;
Ok(self)
}
pub fn valid_until_timestamp(mut self, ts: Option<Timestamp>) -> Result<Self, InvalidWindow> {
self.validity_window.to_timestamp = ts;
self.validity_window.check_window()?;
Ok(self)
}
}
/// Representation of a number as `lo + hi * 2^128`.

View File

@ -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<HashMap<AccountId, Account>, 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);
}

View File

@ -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<HashMap<AccountId, Account>, 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(_))));
}
}

View File

@ -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::<Timestamp>, None::<Timestamp>);
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::<Timestamp>, None::<Timestamp>);
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,

View File

@ -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(),

View File

@ -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());

View File

@ -162,17 +162,24 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
(sequencer_core, mempool_handle)
}
fn next_block_timestamp_ms(&self) -> 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<NSSATransaction, nssa::error::NssaError> {
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),

View File

@ -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<BlockId>,
Option<BlockId>,
Option<Timestamp>,
Option<Timestamp>,
);
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();
}