Merge pull request #404 from logos-blockchain/feature/validity-window-timestamps

feat: extend ValidityWindow with Unix timestamp bounds
This commit is contained in:
Sergio Chouhy 2026-03-31 16:46:58 -03:00 committed by GitHub
commit 9fa541f3d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 582 additions and 279 deletions

31
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",
]
@ -1522,6 +1515,7 @@ dependencies = [
"log",
"logos-blockchain-common-http-client",
"nssa",
"nssa_core",
"serde",
"serde_with",
"sha2",
@ -3977,7 +3971,6 @@ dependencies = [
"nssa",
"nssa_core",
"rand 0.8.5",
"secp256k1",
"serde",
"sha2",
"thiserror 2.0.18",
@ -5269,13 +5262,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 +7079,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"

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View File

@ -1,4 +1,5 @@
use borsh::{BorshDeserialize, BorshSerialize};
use nssa_core::{BlockId, Timestamp};
use serde::{Deserialize, Serialize};
use sha2::{Digest as _, Sha256, digest::FixedOutput as _};
@ -6,8 +7,6 @@ use crate::{HashType, transaction::NSSATransaction};
pub type MantleMsgId = [u8; 32];
pub type BlockHash = HashType;
pub type BlockId = u64;
pub type TimeStamp = u64;
#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
pub struct BlockMeta {
@ -35,7 +34,7 @@ pub struct BlockHeader {
pub block_id: BlockId,
pub prev_block_hash: BlockHash,
pub hash: BlockHash,
pub timestamp: TimeStamp,
pub timestamp: Timestamp,
pub signature: nssa::Signature,
}
@ -75,7 +74,7 @@ impl<'de> Deserialize<'de> for Block {
pub struct HashableBlockData {
pub block_id: BlockId,
pub prev_block_hash: BlockHash,
pub timestamp: TimeStamp,
pub timestamp: Timestamp,
pub transactions: Vec<NSSATransaction>,
}

View File

@ -1,9 +1,10 @@
use borsh::{BorshDeserialize, BorshSerialize};
use log::warn;
use nssa::{AccountId, V03State};
use nssa_core::{BlockId, Timestamp};
use serde::{Deserialize, Serialize};
use crate::{HashType, block::BlockId};
use crate::HashType;
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub enum NSSATransaction {
@ -69,11 +70,12 @@ impl NSSATransaction {
self,
state: &mut V03State,
block_id: BlockId,
timestamp: Timestamp,
) -> Result<Self, nssa::error::NssaError> {
match &self {
Self::Public(tx) => state.transition_from_public_transaction(tx, block_id),
Self::Public(tx) => state.transition_from_public_transaction(tx, block_id, timestamp),
Self::PrivacyPreserving(tx) => {
state.transition_from_privacy_preserving_transaction(tx, block_id)
state.transition_from_privacy_preserving_transaction(tx, block_id, timestamp)
}
Self::ProgramDeployment(tx) => state.transition_from_program_deployment_transaction(tx),
}

View File

@ -177,13 +177,13 @@ pub fn TransactionPage() -> impl IntoView {
encrypted_private_post_states,
new_commitments,
new_nullifiers,
validity_window
block_validity_window,
timestamp_validity_window,
} = message;
let WitnessSet {
signatures_and_public_keys: _,
proof,
} = witness_set;
let proof_len = proof.map_or(0, |p| p.0.len());
view! {
<div class="transaction-details">
@ -214,8 +214,12 @@ pub fn TransactionPage() -> impl IntoView {
<span class="info-value">{format!("{proof_len} bytes")}</span>
</div>
<div class="info-row">
<span class="info-label">"Validity Window:"</span>
<span class="info-value">{validity_window.to_string()}</span>
<span class="info-label">"Block Validity Window:"</span>
<span class="info-value">{block_validity_window.to_string()}</span>
</div>
<div class="info-row">
<span class="info-label">"Timestamp Validity Window:"</span>
<span class="info-value">{timestamp_validity_window.to_string()}</span>
</div>
</div>

View File

@ -3,10 +3,11 @@ use std::{path::Path, sync::Arc};
use anyhow::Result;
use bedrock_client::HeaderId;
use common::{
block::{BedrockStatus, Block, BlockId},
block::{BedrockStatus, Block},
transaction::NSSATransaction,
};
use nssa::{Account, AccountId, V03State};
use nssa_core::BlockId;
use storage::indexer::RocksDBIO;
use tokio::sync::RwLock;
@ -125,7 +126,11 @@ 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

@ -287,7 +287,8 @@ impl From<nssa::privacy_preserving_transaction::message::Message> for PrivacyPre
encrypted_private_post_states,
new_commitments,
new_nullifiers,
validity_window,
block_validity_window,
timestamp_validity_window,
} = value;
Self {
public_account_ids: public_account_ids.into_iter().map(Into::into).collect(),
@ -302,7 +303,8 @@ 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(),
block_validity_window: block_validity_window.into(),
timestamp_validity_window: timestamp_validity_window.into(),
}
}
}
@ -318,7 +320,8 @@ impl TryFrom<PrivacyPreservingMessage> for nssa::privacy_preserving_transaction:
encrypted_private_post_states,
new_commitments,
new_nullifiers,
validity_window,
block_validity_window,
timestamp_validity_window,
} = value;
Ok(Self {
public_account_ids: public_account_ids.into_iter().map(Into::into).collect(),
@ -340,7 +343,10 @@ impl TryFrom<PrivacyPreservingMessage> for nssa::privacy_preserving_transaction:
.into_iter()
.map(|(n, d)| (n.into(), d.into()))
.collect(),
validity_window: validity_window
block_validity_window: block_validity_window
.try_into()
.map_err(|e| nssa::error::NssaError::InvalidInput(format!("{e}")))?,
timestamp_validity_window: timestamp_validity_window
.try_into()
.map_err(|e| nssa::error::NssaError::InvalidInput(format!("{e}")))?,
})
@ -692,13 +698,13 @@ impl From<HashType> for common::HashType {
// ValidityWindow conversions
// ============================================================================
impl From<nssa_core::program::ValidityWindow> for ValidityWindow {
fn from(value: nssa_core::program::ValidityWindow) -> Self {
impl From<nssa_core::program::ValidityWindow<u64>> for ValidityWindow {
fn from(value: nssa_core::program::ValidityWindow<u64>) -> Self {
Self((value.start(), value.end()))
}
}
impl TryFrom<ValidityWindow> for nssa_core::program::ValidityWindow {
impl TryFrom<ValidityWindow> for nssa_core::program::ValidityWindow<u64> {
type Error = nssa_core::program::InvalidWindow;
fn try_from(value: ValidityWindow) -> Result<Self, Self::Error> {

View File

@ -235,7 +235,8 @@ pub struct PrivacyPreservingMessage {
pub encrypted_private_post_states: Vec<EncryptedAccountData>,
pub new_commitments: Vec<Commitment>,
pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>,
pub validity_window: ValidityWindow,
pub block_validity_window: ValidityWindow,
pub timestamp_validity_window: ValidityWindow,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]

View File

@ -124,7 +124,8 @@ impl MockIndexerService {
indexer_service_protocol::Nullifier([tx_idx as u8; 32]),
CommitmentSetDigest([0xff; 32]),
)],
validity_window: ValidityWindow((None, None)),
block_validity_window: ValidityWindow((None, None)),
timestamp_validity_window: ValidityWindow((None, None)),
},
witness_set: WitnessSet {
signatures_and_public_keys: vec![],

View File

@ -5,7 +5,7 @@ use crate::{
NullifierSecretKey, SharedSecretKey,
account::{Account, AccountWithMetadata},
encryption::Ciphertext,
program::{ProgramId, ProgramOutput, ValidityWindow},
program::{BlockValidityWindow, ProgramId, ProgramOutput, TimestampValidityWindow},
};
#[derive(Serialize, Deserialize)]
@ -36,7 +36,8 @@ pub struct PrivacyPreservingCircuitOutput {
pub ciphertexts: Vec<Ciphertext>,
pub new_commitments: Vec<Commitment>,
pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>,
pub validity_window: ValidityWindow,
pub block_validity_window: BlockValidityWindow,
pub timestamp_validity_window: TimestampValidityWindow,
}
#[cfg(feature = "host")]
@ -102,7 +103,8 @@ mod tests {
),
[0xab; 32],
)],
validity_window: (Some(1), None).try_into().unwrap(),
block_validity_window: (1..).into(),
timestamp_validity_window: TimestampValidityWindow::new_unbounded(),
};
let bytes = output.to_bytes();
let output_from_slice: PrivacyPreservingCircuitOutput = from_slice(&bytes).unwrap();

View File

@ -21,3 +21,7 @@ pub mod program;
#[cfg(feature = "host")]
pub mod error;
pub type BlockId = u64;
/// Unix timestamp in milliseconds.
pub type Timestamp = u64;

View File

@ -5,7 +5,10 @@ use borsh::{BorshDeserialize, BorshSerialize};
use risc0_zkvm::{DeserializeOwned, guest::env, serde::Deserializer};
use serde::{Deserialize, Serialize};
use crate::account::{Account, AccountId, AccountWithMetadata};
use crate::{
BlockId, Timestamp,
account::{Account, AccountId, AccountWithMetadata},
};
pub const DEFAULT_PROGRAM_ID: ProgramId = [0; 8];
pub const MAX_NUMBER_CHAINED_CALLS: usize = 10;
@ -171,20 +174,21 @@ impl AccountPostState {
}
}
pub type BlockId = u64;
pub type BlockValidityWindow = ValidityWindow<BlockId>;
pub type TimestampValidityWindow = ValidityWindow<Timestamp>;
#[derive(Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(
any(feature = "host", test),
derive(Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)
)]
pub struct ValidityWindow {
from: Option<BlockId>,
to: Option<BlockId>,
pub struct ValidityWindow<T> {
from: Option<T>,
to: Option<T>,
}
impl ValidityWindow {
/// Creates a window with no bounds, valid for every block ID.
impl<T> ValidityWindow<T> {
/// Creates a window with no bounds.
#[must_use]
pub const fn new_unbounded() -> Self {
Self {
@ -192,42 +196,42 @@ impl ValidityWindow {
to: None,
}
}
}
/// 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.
impl<T: Copy + PartialOrd> ValidityWindow<T> {
/// Valid for values in the range [from, to), where `from` is included and `to` is excluded.
#[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)
pub fn is_valid_for(&self, value: T) -> bool {
self.from.is_none_or(|start| value >= start) && self.to.is_none_or(|end| value < end)
}
/// Returns `Err(InvalidWindow)` if both bounds are set and `from >= to`.
const fn check_window(&self) -> Result<(), InvalidWindow> {
if let (Some(from_id), Some(until_id)) = (self.from, self.to)
&& from_id >= until_id
fn check_window(&self) -> Result<(), InvalidWindow> {
if let (Some(from), Some(to)) = (self.from, self.to)
&& from >= to
{
Err(InvalidWindow)
} else {
Ok(())
return Err(InvalidWindow);
}
Ok(())
}
/// Inclusive lower bound. `None` means the window starts at the beginning of the chain.
/// Inclusive lower bound. `None` means no lower bound.
#[must_use]
pub const fn start(&self) -> Option<BlockId> {
pub const fn start(&self) -> Option<T> {
self.from
}
/// Exclusive upper bound. `None` means the window has no expiry.
/// Exclusive upper bound. `None` means no upper bound.
#[must_use]
pub const fn end(&self) -> Option<BlockId> {
pub const fn end(&self) -> Option<T> {
self.to
}
}
impl TryFrom<(Option<BlockId>, Option<BlockId>)> for ValidityWindow {
impl<T: Copy + PartialOrd> TryFrom<(Option<T>, Option<T>)> for ValidityWindow<T> {
type Error = InvalidWindow;
fn try_from(value: (Option<BlockId>, Option<BlockId>)) -> Result<Self, Self::Error> {
fn try_from(value: (Option<T>, Option<T>)) -> Result<Self, Self::Error> {
let this = Self {
from: value.0,
to: value.1,
@ -237,16 +241,16 @@ impl TryFrom<(Option<BlockId>, Option<BlockId>)> for ValidityWindow {
}
}
impl TryFrom<std::ops::Range<BlockId>> for ValidityWindow {
impl<T: Copy + PartialOrd> TryFrom<std::ops::Range<T>> for ValidityWindow<T> {
type Error = InvalidWindow;
fn try_from(value: std::ops::Range<BlockId>) -> Result<Self, Self::Error> {
fn try_from(value: std::ops::Range<T>) -> Result<Self, Self::Error> {
(Some(value.start), Some(value.end)).try_into()
}
}
impl From<std::ops::RangeFrom<BlockId>> for ValidityWindow {
fn from(value: std::ops::RangeFrom<BlockId>) -> Self {
impl<T: Copy + PartialOrd> From<std::ops::RangeFrom<T>> for ValidityWindow<T> {
fn from(value: std::ops::RangeFrom<T>) -> Self {
Self {
from: Some(value.start),
to: None,
@ -254,8 +258,8 @@ impl From<std::ops::RangeFrom<BlockId>> for ValidityWindow {
}
}
impl From<std::ops::RangeTo<BlockId>> for ValidityWindow {
fn from(value: std::ops::RangeTo<BlockId>) -> Self {
impl<T: Copy + PartialOrd> From<std::ops::RangeTo<T>> for ValidityWindow<T> {
fn from(value: std::ops::RangeTo<T>) -> Self {
Self {
from: None,
to: Some(value.end),
@ -263,7 +267,7 @@ impl From<std::ops::RangeTo<BlockId>> for ValidityWindow {
}
}
impl From<std::ops::RangeFull> for ValidityWindow {
impl<T> From<std::ops::RangeFull> for ValidityWindow<T> {
fn from(_: std::ops::RangeFull) -> Self {
Self::new_unbounded()
}
@ -285,8 +289,10 @@ pub struct ProgramOutput {
pub post_states: Vec<AccountPostState>,
/// The list of chained calls to other programs.
pub chained_calls: Vec<ChainedCall>,
/// The window where the program output is valid.
pub validity_window: ValidityWindow,
/// The block ID window where the program output is valid.
pub block_validity_window: BlockValidityWindow,
/// The timestamp window where the program output is valid.
pub timestamp_validity_window: TimestampValidityWindow,
}
impl ProgramOutput {
@ -300,7 +306,8 @@ impl ProgramOutput {
pre_states,
post_states,
chained_calls: Vec::new(),
validity_window: ValidityWindow::new_unbounded(),
block_validity_window: ValidityWindow::new_unbounded(),
timestamp_validity_window: ValidityWindow::new_unbounded(),
}
}
@ -313,19 +320,52 @@ impl ProgramOutput {
self
}
/// Sets the validity window from an infallible range conversion (`1..`, `..5`, `..`).
pub fn with_validity_window<W: Into<ValidityWindow>>(mut self, window: W) -> Self {
self.validity_window = window.into();
/// Sets the block ID validity window from an infallible range conversion (`1..`, `..5`, `..`).
pub fn with_block_validity_window<W: Into<BlockValidityWindow>>(mut self, window: W) -> Self {
self.block_validity_window = window.into();
self
}
/// Sets the validity window from a fallible range conversion (`1..5`).
/// Sets the block ID validity window from a fallible range conversion (`1..5`).
/// Returns `Err` if the range is empty.
pub fn try_with_validity_window<W: TryInto<ValidityWindow, Error = InvalidWindow>>(
pub fn try_with_block_validity_window<
W: TryInto<BlockValidityWindow, Error = InvalidWindow>,
>(
mut self,
window: W,
) -> Result<Self, InvalidWindow> {
self.validity_window = window.try_into()?;
self.block_validity_window = window.try_into()?;
Ok(self)
}
/// Sets the timestamp validity window from an infallible range conversion.
pub fn with_timestamp_validity_window<W: Into<TimestampValidityWindow>>(
mut self,
window: W,
) -> Self {
self.timestamp_validity_window = window.into();
self
}
/// Sets the timestamp validity window from a fallible range conversion.
/// Returns `Err` if the range is empty.
pub fn try_with_timestamp_validity_window<
W: TryInto<TimestampValidityWindow, Error = InvalidWindow>,
>(
mut self,
window: W,
) -> Result<Self, InvalidWindow> {
self.timestamp_validity_window = window.try_into()?;
Ok(self)
}
pub fn valid_from_timestamp(mut self, ts: Option<Timestamp>) -> Result<Self, InvalidWindow> {
self.timestamp_validity_window = (ts, self.timestamp_validity_window.end()).try_into()?;
Ok(self)
}
pub fn valid_until_timestamp(mut self, ts: Option<Timestamp>) -> Result<Self, InvalidWindow> {
self.timestamp_validity_window = (self.timestamp_validity_window.start(), ts).try_into()?;
Ok(self)
}
}
@ -482,128 +522,131 @@ mod tests {
use super::*;
#[test]
fn validity_window_unbounded_accepts_any_block() {
let w = ValidityWindow::new_unbounded();
assert!(w.is_valid_for_block_id(0));
assert!(w.is_valid_for_block_id(u64::MAX));
fn validity_window_unbounded_accepts_any_value() {
let w: ValidityWindow<u64> = ValidityWindow::new_unbounded();
assert!(w.is_valid_for(0));
assert!(w.is_valid_for(u64::MAX));
}
#[test]
fn validity_window_bounded_range_includes_from_excludes_to() {
let w: ValidityWindow = (Some(5), Some(10)).try_into().unwrap();
assert!(!w.is_valid_for_block_id(4));
assert!(w.is_valid_for_block_id(5));
assert!(w.is_valid_for_block_id(9));
assert!(!w.is_valid_for_block_id(10));
let w: ValidityWindow<u64> = (Some(5), Some(10)).try_into().unwrap();
assert!(!w.is_valid_for(4));
assert!(w.is_valid_for(5));
assert!(w.is_valid_for(9));
assert!(!w.is_valid_for(10));
}
#[test]
fn validity_window_only_from_bound() {
let w: ValidityWindow = (Some(5), None).try_into().unwrap();
assert!(!w.is_valid_for_block_id(4));
assert!(w.is_valid_for_block_id(5));
assert!(w.is_valid_for_block_id(u64::MAX));
let w: ValidityWindow<u64> = (Some(5), None).try_into().unwrap();
assert!(!w.is_valid_for(4));
assert!(w.is_valid_for(5));
assert!(w.is_valid_for(u64::MAX));
}
#[test]
fn validity_window_only_to_bound() {
let w: ValidityWindow = (None, Some(5)).try_into().unwrap();
assert!(w.is_valid_for_block_id(0));
assert!(w.is_valid_for_block_id(4));
assert!(!w.is_valid_for_block_id(5));
let w: ValidityWindow<u64> = (None, Some(5)).try_into().unwrap();
assert!(w.is_valid_for(0));
assert!(w.is_valid_for(4));
assert!(!w.is_valid_for(5));
}
#[test]
fn validity_window_adjacent_bounds_are_invalid() {
// [5, 5) is an empty range — from == to
assert!(ValidityWindow::try_from((Some(5), Some(5))).is_err());
assert!(ValidityWindow::<u64>::try_from((Some(5), Some(5))).is_err());
}
#[test]
fn validity_window_inverted_bounds_are_invalid() {
assert!(ValidityWindow::try_from((Some(10), Some(5))).is_err());
assert!(ValidityWindow::<u64>::try_from((Some(10), Some(5))).is_err());
}
#[test]
fn validity_window_getters_match_construction() {
let w: ValidityWindow = (Some(3), Some(7)).try_into().unwrap();
let w: ValidityWindow<u64> = (Some(3), Some(7)).try_into().unwrap();
assert_eq!(w.start(), Some(3));
assert_eq!(w.end(), Some(7));
}
#[test]
fn validity_window_getters_for_unbounded() {
let w = ValidityWindow::new_unbounded();
let w: ValidityWindow<u64> = ValidityWindow::new_unbounded();
assert_eq!(w.start(), None);
assert_eq!(w.end(), None);
}
#[test]
fn validity_window_from_range() {
let w = ValidityWindow::try_from(5_u64..10).unwrap();
let w: ValidityWindow<u64> = ValidityWindow::try_from(5_u64..10).unwrap();
assert_eq!(w.start(), Some(5));
assert_eq!(w.end(), Some(10));
}
#[test]
fn validity_window_from_range_empty_is_invalid() {
assert!(ValidityWindow::try_from(5_u64..5).is_err());
assert!(ValidityWindow::<u64>::try_from(5_u64..5).is_err());
}
#[test]
fn validity_window_from_range_inverted_is_invalid() {
let from = 10_u64;
let to = 5_u64;
assert!(ValidityWindow::try_from(from..to).is_err());
assert!(ValidityWindow::<u64>::try_from(from..to).is_err());
}
#[test]
fn validity_window_from_range_from() {
let w: ValidityWindow = (5_u64..).into();
let w: ValidityWindow<u64> = (5_u64..).into();
assert_eq!(w.start(), Some(5));
assert_eq!(w.end(), None);
}
#[test]
fn validity_window_from_range_to() {
let w: ValidityWindow = (..10_u64).into();
let w: ValidityWindow<u64> = (..10_u64).into();
assert_eq!(w.start(), None);
assert_eq!(w.end(), Some(10));
}
#[test]
fn validity_window_from_range_full() {
let w: ValidityWindow = (..).into();
let w: ValidityWindow<u64> = (..).into();
assert_eq!(w.start(), None);
assert_eq!(w.end(), None);
}
#[test]
fn program_output_try_with_validity_window_range() {
fn program_output_try_with_block_validity_window_range() {
let output = ProgramOutput::new(vec![], vec![], vec![])
.try_with_validity_window(10_u64..100)
.try_with_block_validity_window(10_u64..100)
.unwrap();
assert_eq!(output.validity_window.start(), Some(10));
assert_eq!(output.validity_window.end(), Some(100));
assert_eq!(output.block_validity_window.start(), Some(10));
assert_eq!(output.block_validity_window.end(), Some(100));
}
#[test]
fn program_output_with_validity_window_range_from() {
let output = ProgramOutput::new(vec![], vec![], vec![]).with_validity_window(10_u64..);
assert_eq!(output.validity_window.start(), Some(10));
assert_eq!(output.validity_window.end(), None);
fn program_output_with_block_validity_window_range_from() {
let output =
ProgramOutput::new(vec![], vec![], vec![]).with_block_validity_window(10_u64..);
assert_eq!(output.block_validity_window.start(), Some(10));
assert_eq!(output.block_validity_window.end(), None);
}
#[test]
fn program_output_with_validity_window_range_to() {
let output = ProgramOutput::new(vec![], vec![], vec![]).with_validity_window(..100_u64);
assert_eq!(output.validity_window.start(), None);
assert_eq!(output.validity_window.end(), Some(100));
fn program_output_with_block_validity_window_range_to() {
let output =
ProgramOutput::new(vec![], vec![], vec![]).with_block_validity_window(..100_u64);
assert_eq!(output.block_validity_window.start(), None);
assert_eq!(output.block_validity_window.end(), Some(100));
}
#[test]
fn program_output_try_with_validity_window_empty_range_fails() {
let result = ProgramOutput::new(vec![], vec![], vec![]).try_with_validity_window(5_u64..5);
fn program_output_try_with_block_validity_window_empty_range_fails() {
let result =
ProgramOutput::new(vec![], vec![], vec![]).try_with_block_validity_window(5_u64..5);
assert!(result.is_err());
}

View File

@ -3,7 +3,7 @@ use nssa_core::{
Commitment, CommitmentSetDigest, Nullifier, NullifierPublicKey, PrivacyPreservingCircuitOutput,
account::{Account, Nonce},
encryption::{Ciphertext, EphemeralPublicKey, ViewingPublicKey},
program::ValidityWindow,
program::{BlockValidityWindow, TimestampValidityWindow},
};
use sha2::{Digest as _, Sha256};
@ -53,7 +53,8 @@ pub struct Message {
pub encrypted_private_post_states: Vec<EncryptedAccountData>,
pub new_commitments: Vec<Commitment>,
pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>,
pub validity_window: ValidityWindow,
pub block_validity_window: BlockValidityWindow,
pub timestamp_validity_window: TimestampValidityWindow,
}
impl std::fmt::Debug for Message {
@ -79,7 +80,8 @@ impl std::fmt::Debug for Message {
)
.field("new_commitments", &self.new_commitments)
.field("new_nullifiers", &nullifiers)
.field("validity_window", &self.validity_window)
.field("block_validity_window", &self.block_validity_window)
.field("timestamp_validity_window", &self.timestamp_validity_window)
.finish()
}
}
@ -112,7 +114,8 @@ impl Message {
encrypted_private_post_states,
new_commitments: output.new_commitments,
new_nullifiers: output.new_nullifiers,
validity_window: output.validity_window,
block_validity_window: output.block_validity_window,
timestamp_validity_window: output.timestamp_validity_window,
})
}
}
@ -123,6 +126,7 @@ pub mod tests {
Commitment, EncryptionScheme, Nullifier, NullifierPublicKey, SharedSecretKey,
account::Account,
encryption::{EphemeralPublicKey, ViewingPublicKey},
program::{BlockValidityWindow, TimestampValidityWindow},
};
use sha2::{Digest as _, Sha256};
@ -165,7 +169,8 @@ pub mod tests {
encrypted_private_post_states,
new_commitments,
new_nullifiers,
validity_window: (None, None).try_into().unwrap(),
block_validity_window: BlockValidityWindow::new_unbounded(),
timestamp_validity_window: TimestampValidityWindow::new_unbounded(),
}
}

View File

@ -5,17 +5,14 @@ use std::{
use borsh::{BorshDeserialize, BorshSerialize};
use nssa_core::{
Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput,
BlockId, PrivacyPreservingCircuitOutput, Timestamp,
account::{Account, AccountWithMetadata},
program::{BlockId, ValidityWindow},
};
use sha2::{Digest as _, digest::FixedOutput as _};
use super::{message::Message, witness_set::WitnessSet};
use crate::{
AccountId, V03State,
error::NssaError,
privacy_preserving_transaction::{circuit::Proof, message::EncryptedAccountData},
AccountId, V03State, error::NssaError, privacy_preserving_transaction::circuit::Proof,
};
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
@ -37,6 +34,7 @@ impl PrivacyPreservingTransaction {
&self,
state: &V03State,
block_id: BlockId,
timestamp: Timestamp,
) -> Result<HashMap<AccountId, Account>, NssaError> {
let message = &self.message;
let witness_set = &self.witness_set;
@ -94,7 +92,9 @@ impl PrivacyPreservingTransaction {
}
// Verify validity window
if !message.validity_window.is_valid_for_block_id(block_id) {
if !message.block_validity_window.is_valid_for(block_id)
|| !message.timestamp_validity_window.is_valid_for(timestamp)
{
return Err(NssaError::OutOfValidityWindow);
}
@ -115,11 +115,7 @@ impl PrivacyPreservingTransaction {
check_privacy_preserving_circuit_proof_is_valid(
&witness_set.proof,
&public_pre_states,
&message.public_post_states,
&message.encrypted_private_post_states,
&message.new_commitments,
&message.new_nullifiers,
&message.validity_window,
message,
)?;
// 5. Commitment freshness
@ -177,23 +173,21 @@ impl PrivacyPreservingTransaction {
fn check_privacy_preserving_circuit_proof_is_valid(
proof: &Proof,
public_pre_states: &[AccountWithMetadata],
public_post_states: &[Account],
encrypted_private_post_states: &[EncryptedAccountData],
new_commitments: &[Commitment],
new_nullifiers: &[(Nullifier, CommitmentSetDigest)],
validity_window: &ValidityWindow,
message: &Message,
) -> Result<(), NssaError> {
let output = PrivacyPreservingCircuitOutput {
public_pre_states: public_pre_states.to_vec(),
public_post_states: public_post_states.to_vec(),
ciphertexts: encrypted_private_post_states
public_post_states: message.public_post_states.clone(),
ciphertexts: message
.encrypted_private_post_states
.iter()
.cloned()
.map(|value| value.ciphertext)
.collect(),
new_commitments: new_commitments.to_vec(),
new_nullifiers: new_nullifiers.to_vec(),
validity_window: validity_window.to_owned(),
new_commitments: message.new_commitments.clone(),
new_nullifiers: message.new_nullifiers.clone(),
block_validity_window: message.block_validity_window,
timestamp_validity_window: message.timestamp_validity_window,
};
proof
.is_valid_for(&output)

View File

@ -3,8 +3,9 @@ use std::collections::{HashMap, HashSet, VecDeque};
use borsh::{BorshDeserialize, BorshSerialize};
use log::debug;
use nssa_core::{
BlockId, Timestamp,
account::{Account, AccountId, AccountWithMetadata},
program::{BlockId, ChainedCall, Claim, DEFAULT_PROGRAM_ID, validate_execution},
program::{ChainedCall, Claim, DEFAULT_PROGRAM_ID, validate_execution},
};
use sha2::{Digest as _, digest::FixedOutput as _};
@ -71,6 +72,7 @@ impl PublicTransaction {
&self,
state: &V03State,
block_id: BlockId,
timestamp: Timestamp,
) -> Result<HashMap<AccountId, Account>, NssaError> {
let message = self.message();
let witness_set = self.witness_set();
@ -195,9 +197,10 @@ impl PublicTransaction {
// Verify validity window
ensure!(
program_output
.validity_window
.is_valid_for_block_id(block_id),
program_output.block_validity_window.is_valid_for(block_id)
&& program_output
.timestamp_validity_window
.is_valid_for(timestamp),
NssaError::OutOfValidityWindow
);
@ -388,7 +391,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 +411,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 +432,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 +452,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 +468,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

@ -2,9 +2,10 @@ use std::collections::{BTreeSet, HashMap, HashSet};
use borsh::{BorshDeserialize, BorshSerialize};
use nssa_core::{
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, Nullifier,
BlockId, Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, Nullifier,
Timestamp,
account::{Account, AccountId, Nonce},
program::{BlockId, ProgramId},
program::ProgramId,
};
use crate::{
@ -159,8 +160,9 @@ impl V03State {
&mut self,
tx: &PublicTransaction,
block_id: BlockId,
timestamp: 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)?;
#[expect(
clippy::iter_over_hash_type,
@ -184,9 +186,11 @@ impl V03State {
&mut self,
tx: &PrivacyPreservingTransaction,
block_id: BlockId,
timestamp: 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)?;
let message = tx.message();
@ -338,10 +342,11 @@ pub mod tests {
use std::collections::HashMap;
use nssa_core::{
Commitment, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey,
BlockId, Commitment, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey,
Timestamp,
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
encryption::{EphemeralPublicKey, Scalar, ViewingPublicKey},
program::{BlockId, PdaSeed, ProgramId, ValidityWindow},
program::{BlockValidityWindow, PdaSeed, ProgramId, TimestampValidityWindow},
};
use crate::{
@ -576,7 +581,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 +603,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 +628,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 +657,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 +668,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 +690,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 +707,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 +724,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 +748,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 +772,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 +796,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 +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 +848,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 +873,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 +891,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 +920,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 +1113,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 +1183,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 +1247,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 +2175,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 +2193,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 +2271,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);
@ -2288,7 +2293,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::ProgramExecutionFailed(_))));
assert_eq!(state.get_account_by_id(account_id), Account::default());
@ -2313,7 +2318,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&account_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();
assert_eq!(
state.get_account_by_id(account_id),
@ -2361,7 +2366,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 +2406,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 +2447,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 +2504,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);
@ -2572,7 +2577,7 @@ pub mod tests {
let tx = PrivacyPreservingTransaction::new(message, witness_set);
state
.transition_from_privacy_preserving_transaction(&tx, 1)
.transition_from_privacy_preserving_transaction(&tx, 1, 0)
.unwrap();
let nullifier = Nullifier::for_account_update(&sender_commitment, &sender_keys.nsk);
@ -2690,7 +2695,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 +2711,96 @@ 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);
// Set up the token accounts directly (bypassing public transactions which
// would require signers for Claim::Authorized). The focus of this test is
// the PDA mechanism in the pinata program's chained call, not token creation.
let total_supply: u128 = 10_000_000;
let token_definition = token_core::TokenDefinition::Fungible {
name: String::from("PINATA"),
total_supply,
metadata_id: None,
};
let token_holding = token_core::TokenHolding::Fungible {
definition_id: pinata_token_definition_id,
balance: total_supply,
};
let winner_holding = token_core::TokenHolding::Fungible {
definition_id: pinata_token_definition_id,
balance: 0,
};
state.force_insert_account(
pinata_token_definition_id,
Account {
program_owner: token.id(),
data: Data::from(&token_definition),
..Account::default()
},
);
state.force_insert_account(
pinata_token_holding_id,
Account {
program_owner: token.id(),
data: Data::from(&token_holding),
..Account::default()
},
);
state.force_insert_account(
winner_token_holding_id,
Account {
program_owner: token.id(),
data: Data::from(&winner_holding),
..Account::default()
},
);
// 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 +2822,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 +2868,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 +2937,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());
@ -2889,7 +2984,7 @@ pub mod tests {
let tx = PrivacyPreservingTransaction::new(message, witness_set);
state
.transition_from_privacy_preserving_transaction(&tx, 1)
.transition_from_privacy_preserving_transaction(&tx, 1, 0)
.unwrap();
let nullifier = Nullifier::for_account_initialization(&private_keys.npk());
@ -2942,7 +3037,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 +3086,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 +3111,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)));
@ -3145,7 +3240,7 @@ pub mod tests {
validity_window: (Option<BlockId>, Option<BlockId>),
block_id: BlockId,
) {
let validity_window: ValidityWindow = validity_window.try_into().unwrap();
let block_validity_window: BlockValidityWindow = validity_window.try_into().unwrap();
let validity_window_program = Program::validity_window();
let account_keys = test_public_account_keys_1();
let pre = AccountWithMetadata::new(Account::default(), false, account_keys.account_id());
@ -3154,21 +3249,76 @@ pub mod tests {
let account_ids = vec![pre.account_id];
let nonces = vec![];
let program_id = validity_window_program.id();
let message = public_transaction::Message::try_new(
program_id,
account_ids,
nonces,
validity_window,
)
.unwrap();
let instruction = (
block_validity_window,
TimestampValidityWindow::new_unbounded(),
);
let message =
public_transaction::Message::try_new(program_id, account_ids, nonces, 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 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,
(None, Some(e)) => block_id < e,
let result = state.transition_from_public_transaction(&tx, block_id, 0);
let is_inside_validity_window =
match (block_validity_window.start(), block_validity_window.end()) {
(Some(s), Some(e)) => s <= block_id && block_id < e,
(Some(s), None) => s <= block_id,
(None, Some(e)) => block_id < e,
(None, None) => true,
};
if is_inside_validity_window {
assert!(result.is_ok());
} else {
assert!(matches!(result, Err(NssaError::OutOfValidityWindow)));
}
}
#[test_case::test_case((Some(1), Some(3)), 3; "at upper bound")]
#[test_case::test_case((Some(1), Some(3)), 2; "inside range")]
#[test_case::test_case((Some(1), Some(3)), 0; "below range")]
#[test_case::test_case((Some(1), Some(3)), 1; "at lower bound")]
#[test_case::test_case((Some(1), Some(3)), 4; "above range")]
#[test_case::test_case((Some(1), None), 1; "lower bound only - at bound")]
#[test_case::test_case((Some(1), None), 10; "lower bound only - above")]
#[test_case::test_case((Some(1), None), 0; "lower bound only - below")]
#[test_case::test_case((None, Some(3)), 3; "upper bound only - at bound")]
#[test_case::test_case((None, Some(3)), 0; "upper bound only - below")]
#[test_case::test_case((None, Some(3)), 4; "upper bound only - above")]
#[test_case::test_case((None, None), 0; "no bounds - always valid")]
#[test_case::test_case((None, None), 100; "no bounds - always valid 2")]
fn timestamp_validity_window_works_in_public_transactions(
validity_window: (Option<Timestamp>, Option<Timestamp>),
timestamp: Timestamp,
) {
let timestamp_validity_window: TimestampValidityWindow =
validity_window.try_into().unwrap();
let validity_window_program = Program::validity_window();
let account_keys = test_public_account_keys_1();
let pre = AccountWithMetadata::new(Account::default(), false, account_keys.account_id());
let mut state = V03State::new_with_genesis_accounts(&[], &[]).with_test_programs();
let tx = {
let account_ids = vec![pre.account_id];
let nonces = vec![];
let program_id = validity_window_program.id();
let instruction = (
BlockValidityWindow::new_unbounded(),
timestamp_validity_window,
);
let message =
public_transaction::Message::try_new(program_id, account_ids, nonces, instruction)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
PublicTransaction::new(message, witness_set)
};
let result = state.transition_from_public_transaction(&tx, 1, timestamp);
let is_inside_validity_window = match (
timestamp_validity_window.start(),
timestamp_validity_window.end(),
) {
(Some(s), Some(e)) => s <= timestamp && timestamp < e,
(Some(s), None) => s <= timestamp,
(None, Some(e)) => timestamp < e,
(None, None) => true,
};
if is_inside_validity_window {
@ -3195,7 +3345,7 @@ pub mod tests {
validity_window: (Option<BlockId>, Option<BlockId>),
block_id: BlockId,
) {
let validity_window: ValidityWindow = validity_window.try_into().unwrap();
let block_validity_window: BlockValidityWindow = validity_window.try_into().unwrap();
let validity_window_program = Program::validity_window();
let account_keys = test_private_account_keys_1();
let pre = AccountWithMetadata::new(Account::default(), false, &account_keys.npk());
@ -3205,9 +3355,13 @@ pub mod tests {
let shared_secret = SharedSecretKey::new(&esk, &account_keys.vpk());
let epk = EphemeralPublicKey::from_scalar(esk);
let instruction = (
block_validity_window,
TimestampValidityWindow::new_unbounded(),
);
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,11 +3381,83 @@ 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 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,
(None, Some(e)) => block_id < e,
let result = state.transition_from_privacy_preserving_transaction(&tx, block_id, 0);
let is_inside_validity_window =
match (block_validity_window.start(), block_validity_window.end()) {
(Some(s), Some(e)) => s <= block_id && block_id < e,
(Some(s), None) => s <= block_id,
(None, Some(e)) => block_id < e,
(None, None) => true,
};
if is_inside_validity_window {
assert!(result.is_ok());
} else {
assert!(matches!(result, Err(NssaError::OutOfValidityWindow)));
}
}
#[test_case::test_case((Some(1), Some(3)), 3; "at upper bound")]
#[test_case::test_case((Some(1), Some(3)), 2; "inside range")]
#[test_case::test_case((Some(1), Some(3)), 0; "below range")]
#[test_case::test_case((Some(1), Some(3)), 1; "at lower bound")]
#[test_case::test_case((Some(1), Some(3)), 4; "above range")]
#[test_case::test_case((Some(1), None), 1; "lower bound only - at bound")]
#[test_case::test_case((Some(1), None), 10; "lower bound only - above")]
#[test_case::test_case((Some(1), None), 0; "lower bound only - below")]
#[test_case::test_case((None, Some(3)), 3; "upper bound only - at bound")]
#[test_case::test_case((None, Some(3)), 0; "upper bound only - below")]
#[test_case::test_case((None, Some(3)), 4; "upper bound only - above")]
#[test_case::test_case((None, None), 0; "no bounds - always valid")]
#[test_case::test_case((None, None), 100; "no bounds - always valid 2")]
fn timestamp_validity_window_works_in_privacy_preserving_transactions(
validity_window: (Option<Timestamp>, Option<Timestamp>),
timestamp: Timestamp,
) {
let timestamp_validity_window: TimestampValidityWindow =
validity_window.try_into().unwrap();
let validity_window_program = Program::validity_window();
let account_keys = test_private_account_keys_1();
let pre = AccountWithMetadata::new(Account::default(), false, &account_keys.npk());
let mut state = V03State::new_with_genesis_accounts(&[], &[]).with_test_programs();
let tx = {
let esk = [3; 32];
let shared_secret = SharedSecretKey::new(&esk, &account_keys.vpk());
let epk = EphemeralPublicKey::from_scalar(esk);
let instruction = (
BlockValidityWindow::new_unbounded(),
timestamp_validity_window,
);
let (output, proof) = circuit::execute_and_prove(
vec![pre],
Program::serialize_instruction(instruction).unwrap(),
vec![2],
vec![(account_keys.npk(), shared_secret)],
vec![],
vec![None],
&validity_window_program.into(),
)
.unwrap();
let message = Message::try_from_circuit_output(
vec![],
vec![],
vec![(account_keys.npk(), account_keys.vpk(), epk)],
output,
)
.unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[]);
PrivacyPreservingTransaction::new(message, witness_set)
};
let result = state.transition_from_privacy_preserving_transaction(&tx, 1, timestamp);
let is_inside_validity_window = match (
timestamp_validity_window.start(),
timestamp_validity_window.end(),
) {
(Some(s), Some(e)) => s <= timestamp && timestamp < e,
(Some(s), None) => s <= timestamp,
(None, Some(e)) => timestamp < e,
(None, None) => true,
};
if is_inside_validity_window {

View File

@ -10,8 +10,9 @@ use nssa_core::{
account::{Account, AccountId, AccountWithMetadata, Nonce},
compute_digest_for_path,
program::{
AccountPostState, ChainedCall, Claim, DEFAULT_PROGRAM_ID, MAX_NUMBER_CHAINED_CALLS,
ProgramId, ProgramOutput, ValidityWindow, validate_execution,
AccountPostState, BlockValidityWindow, ChainedCall, Claim, DEFAULT_PROGRAM_ID,
MAX_NUMBER_CHAINED_CALLS, ProgramId, ProgramOutput, TimestampValidityWindow,
validate_execution,
},
};
use risc0_zkvm::{guest::env, serde::to_vec};
@ -20,7 +21,8 @@ use risc0_zkvm::{guest::env, serde::to_vec};
struct ExecutionState {
pre_states: Vec<AccountWithMetadata>,
post_states: HashMap<AccountId, Account>,
validity_window: ValidityWindow,
block_validity_window: BlockValidityWindow,
timestamp_validity_window: TimestampValidityWindow,
}
impl ExecutionState {
@ -30,23 +32,40 @@ impl ExecutionState {
program_id: ProgramId,
program_outputs: Vec<ProgramOutput>,
) -> Self {
let valid_from_id = program_outputs
let block_valid_from = program_outputs
.iter()
.filter_map(|output| output.validity_window.start())
.filter_map(|output| output.block_validity_window.start())
.max();
let valid_until_id = program_outputs
let block_valid_until = program_outputs
.iter()
.filter_map(|output| output.validity_window.end())
.filter_map(|output| output.block_validity_window.end())
.min();
let ts_valid_from = program_outputs
.iter()
.filter_map(|output| output.timestamp_validity_window.start())
.max();
let ts_valid_until = program_outputs
.iter()
.filter_map(|output| output.timestamp_validity_window.end())
.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 block_validity_window: BlockValidityWindow = (block_valid_from, block_valid_until)
.try_into()
.expect(
"There should be non empty intersection in the program output block validity windows",
);
let timestamp_validity_window: TimestampValidityWindow =
(ts_valid_from, ts_valid_until)
.try_into()
.expect(
"There should be non empty intersection in the program output timestamp validity windows",
);
let mut execution_state = Self {
pre_states: Vec::new(),
post_states: HashMap::new(),
validity_window,
block_validity_window,
timestamp_validity_window,
};
let Some(first_output) = program_outputs.first() else {
@ -277,7 +296,8 @@ fn compute_circuit_output(
ciphertexts: Vec::new(),
new_commitments: Vec::new(),
new_nullifiers: Vec::new(),
validity_window: execution_state.validity_window,
block_validity_window: execution_state.block_validity_window,
timestamp_validity_window: execution_state.timestamp_validity_window,
};
let states_iter = execution_state.into_states_iter();

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

@ -16,6 +16,7 @@ use mempool::{MemPool, MemPoolHandle};
#[cfg(feature = "mock")]
pub use mock::SequencerCoreWithMockClients;
use nssa::V03State;
use nssa_core::{BlockId, Timestamp};
pub use storage::error::DbError;
use testnet_initial_state::initial_state;
@ -165,14 +166,16 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
fn execute_check_transaction_on_state(
&mut self,
tx: NSSATransaction,
block_id: BlockId,
timestamp: Timestamp,
) -> Result<NSSATransaction, nssa::error::NssaError> {
match &tx {
NSSATransaction::Public(tx) => self
.state
.transition_from_public_transaction(tx, self.next_block_id()),
.transition_from_public_transaction(tx, block_id, timestamp),
NSSATransaction::PrivacyPreserving(tx) => self
.state
.transition_from_privacy_preserving_transaction(tx, self.next_block_id()),
.transition_from_privacy_preserving_transaction(tx, block_id, timestamp),
NSSATransaction::ProgramDeployment(tx) => self
.state
.transition_from_program_deployment_transaction(tx),
@ -218,7 +221,7 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
.latest_block_meta()
.context("Failed to get latest block meta from store")?;
let curr_time = u64::try_from(chrono::Utc::now().timestamp_millis())
let new_block_timestamp = u64::try_from(chrono::Utc::now().timestamp_millis())
.expect("Timestamp must be positive");
while let Some(tx) = self.mempool.pop() {
@ -231,7 +234,7 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
block_id: new_block_height,
transactions: temp_valid_transactions,
prev_block_hash: latest_block_meta.hash,
timestamp: curr_time,
timestamp: new_block_timestamp,
};
let block_size = borsh::to_vec(&temp_hashable_data)
@ -249,7 +252,8 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
break;
}
match self.execute_check_transaction_on_state(tx) {
match self.execute_check_transaction_on_state(tx, new_block_height, new_block_timestamp)
{
Ok(valid_tx) => {
valid_transactions.push(valid_tx);
@ -272,7 +276,7 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
block_id: new_block_height,
transactions: valid_transactions,
prev_block_hash: latest_block_meta.hash,
timestamp: curr_time,
timestamp: new_block_timestamp,
};
let block = hashable_data
@ -520,7 +524,7 @@ mod tests {
let tx = tx.transaction_stateless_check().unwrap();
// Signature is not from sender. Execution fails
let result = sequencer.execute_check_transaction_on_state(tx);
let result = sequencer.execute_check_transaction_on_state(tx, 0, 0);
assert!(matches!(
result,
@ -546,7 +550,7 @@ mod tests {
// Passed pre-check
assert!(result.is_ok());
let result = sequencer.execute_check_transaction_on_state(result.unwrap());
let result = sequencer.execute_check_transaction_on_state(result.unwrap(), 0, 0);
let is_failed_at_balance_mismatch = matches!(
result.err().unwrap(),
nssa::error::NssaError::ProgramExecutionFailed(_)
@ -568,7 +572,9 @@ mod tests {
acc1, 0, acc2, 100, &sign_key1,
);
sequencer.execute_check_transaction_on_state(tx).unwrap();
sequencer
.execute_check_transaction_on_state(tx, 0, 0)
.unwrap();
let bal_from = sequencer.state.get_account_by_id(acc1).balance;
let bal_to = sequencer.state.get_account_by_id(acc2).balance;

View File

@ -1,9 +1,5 @@
//! Reexports of types used by sequencer rpc specification.
pub use common::{
HashType,
block::{Block, BlockId},
transaction::NSSATransaction,
};
pub use common::{HashType, block::Block, transaction::NSSATransaction};
pub use nssa::{Account, AccountId, ProgramId};
pub use nssa_core::{Commitment, MembershipProof, account::Nonce};
pub use nssa_core::{BlockId, Commitment, MembershipProof, account::Nonce};

View File

@ -188,7 +188,11 @@ 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:?}"

View File

@ -1,14 +1,15 @@
use nssa_core::program::{
AccountPostState, ProgramInput, ProgramOutput, ValidityWindow, read_nssa_inputs,
AccountPostState, BlockValidityWindow, ProgramInput, ProgramOutput, TimestampValidityWindow,
read_nssa_inputs,
};
type Instruction = ValidityWindow;
type Instruction = (BlockValidityWindow, TimestampValidityWindow);
fn main() {
let (
ProgramInput {
pre_states,
instruction: validity_window,
instruction: (block_validity_window, timestamp_validity_window),
},
instruction_words,
) = read_nssa_inputs::<Instruction>();
@ -24,6 +25,7 @@ fn main() {
vec![pre],
vec![AccountPostState::new(post)],
)
.with_validity_window(validity_window)
.with_block_validity_window(block_validity_window)
.with_timestamp_validity_window(timestamp_validity_window)
.write();
}

View File

@ -1,21 +1,23 @@
use nssa_core::program::{
AccountPostState, ChainedCall, ProgramId, ProgramInput, ProgramOutput, ValidityWindow,
read_nssa_inputs,
AccountPostState, BlockValidityWindow, ChainedCall, ProgramId, ProgramInput, ProgramOutput,
TimestampValidityWindow, read_nssa_inputs,
};
use risc0_zkvm::serde::to_vec;
/// A program that sets a validity window on its output and chains to another program with a
/// potentially different validity window.
/// A program that sets a block validity window on its output and chains to another program with a
/// potentially different block validity window.
///
/// Instruction: (`window`, `chained_program_id`, `chained_window`)
/// The initial output uses `window` and chains to `chained_program_id` with `chained_window`.
type Instruction = (ValidityWindow, ProgramId, ValidityWindow);
/// The chained program (`validity_window`) expects `(BlockValidityWindow, TimestampValidityWindow)`
/// so an unbounded timestamp window is appended automatically.
type Instruction = (BlockValidityWindow, ProgramId, BlockValidityWindow);
fn main() {
let (
ProgramInput {
pre_states,
instruction: (validity_window, chained_program_id, chained_validity_window),
instruction: (block_validity_window, chained_program_id, chained_block_validity_window),
},
instruction_words,
) = read_nssa_inputs::<Instruction>();
@ -23,7 +25,11 @@ fn main() {
let [pre] = <[_; 1]>::try_from(pre_states.clone()).expect("Expected exactly one pre state");
let post = pre.account.clone();
let chained_instruction = to_vec(&chained_validity_window).unwrap();
let chained_instruction = to_vec(&(
chained_block_validity_window,
TimestampValidityWindow::new_unbounded(),
))
.unwrap();
let chained_call = ChainedCall {
program_id: chained_program_id,
instruction_data: chained_instruction,
@ -36,7 +42,7 @@ fn main() {
vec![pre],
vec![AccountPostState::new(post)],
)
.with_validity_window(validity_window)
.with_block_validity_window(block_validity_window)
.with_chained_calls(vec![chained_call])
.write();
}