diff --git a/artifacts/program_methods/amm.bin b/artifacts/program_methods/amm.bin index 478945ed..e90b6736 100644 Binary files a/artifacts/program_methods/amm.bin and b/artifacts/program_methods/amm.bin differ diff --git a/artifacts/program_methods/authenticated_transfer.bin b/artifacts/program_methods/authenticated_transfer.bin index eae3fa1a..035ecefa 100644 Binary files a/artifacts/program_methods/authenticated_transfer.bin and b/artifacts/program_methods/authenticated_transfer.bin differ diff --git a/artifacts/program_methods/pinata.bin b/artifacts/program_methods/pinata.bin index 9574f567..a07d46e4 100644 Binary files a/artifacts/program_methods/pinata.bin and b/artifacts/program_methods/pinata.bin differ diff --git a/artifacts/program_methods/pinata_token.bin b/artifacts/program_methods/pinata_token.bin index 64af9e59..eb45886e 100644 Binary files a/artifacts/program_methods/pinata_token.bin and b/artifacts/program_methods/pinata_token.bin differ diff --git a/artifacts/program_methods/privacy_preserving_circuit.bin b/artifacts/program_methods/privacy_preserving_circuit.bin index dd8791d1..8e0097f4 100644 Binary files a/artifacts/program_methods/privacy_preserving_circuit.bin and b/artifacts/program_methods/privacy_preserving_circuit.bin differ diff --git a/artifacts/program_methods/token.bin b/artifacts/program_methods/token.bin index 98ea8a50..deb446df 100644 Binary files a/artifacts/program_methods/token.bin and b/artifacts/program_methods/token.bin differ diff --git a/artifacts/test_program_methods/burner.bin b/artifacts/test_program_methods/burner.bin index 7b6fb3d3..ee60d8d1 100644 Binary files a/artifacts/test_program_methods/burner.bin and b/artifacts/test_program_methods/burner.bin differ diff --git a/artifacts/test_program_methods/chain_caller.bin b/artifacts/test_program_methods/chain_caller.bin index 92701e88..23d2e289 100644 Binary files a/artifacts/test_program_methods/chain_caller.bin and b/artifacts/test_program_methods/chain_caller.bin differ diff --git a/artifacts/test_program_methods/changer_claimer.bin b/artifacts/test_program_methods/changer_claimer.bin index dc9af2b1..05ff63ed 100644 Binary files a/artifacts/test_program_methods/changer_claimer.bin and b/artifacts/test_program_methods/changer_claimer.bin differ diff --git a/artifacts/test_program_methods/claimer.bin b/artifacts/test_program_methods/claimer.bin index 88bd49ef..fe1236cd 100644 Binary files a/artifacts/test_program_methods/claimer.bin and b/artifacts/test_program_methods/claimer.bin differ diff --git a/artifacts/test_program_methods/data_changer.bin b/artifacts/test_program_methods/data_changer.bin index 28948f6c..3582f149 100644 Binary files a/artifacts/test_program_methods/data_changer.bin and b/artifacts/test_program_methods/data_changer.bin differ diff --git a/artifacts/test_program_methods/extra_output.bin b/artifacts/test_program_methods/extra_output.bin index 854c3de7..0225dbdd 100644 Binary files a/artifacts/test_program_methods/extra_output.bin and b/artifacts/test_program_methods/extra_output.bin differ diff --git a/artifacts/test_program_methods/malicious_authorization_changer.bin b/artifacts/test_program_methods/malicious_authorization_changer.bin index df49f077..7a66be1e 100644 Binary files a/artifacts/test_program_methods/malicious_authorization_changer.bin and b/artifacts/test_program_methods/malicious_authorization_changer.bin differ diff --git a/artifacts/test_program_methods/minter.bin b/artifacts/test_program_methods/minter.bin index a7935759..293f70da 100644 Binary files a/artifacts/test_program_methods/minter.bin and b/artifacts/test_program_methods/minter.bin differ diff --git a/artifacts/test_program_methods/missing_output.bin b/artifacts/test_program_methods/missing_output.bin index 1cd7b1df..8448aed0 100644 Binary files a/artifacts/test_program_methods/missing_output.bin and b/artifacts/test_program_methods/missing_output.bin differ diff --git a/artifacts/test_program_methods/modified_transfer.bin b/artifacts/test_program_methods/modified_transfer.bin index 01e5f3a4..44690ec9 100644 Binary files a/artifacts/test_program_methods/modified_transfer.bin and b/artifacts/test_program_methods/modified_transfer.bin differ diff --git a/artifacts/test_program_methods/nonce_changer.bin b/artifacts/test_program_methods/nonce_changer.bin index 19a89ae9..11709260 100644 Binary files a/artifacts/test_program_methods/nonce_changer.bin and b/artifacts/test_program_methods/nonce_changer.bin differ diff --git a/artifacts/test_program_methods/noop.bin b/artifacts/test_program_methods/noop.bin index 40f54e43..bbc6fe81 100644 Binary files a/artifacts/test_program_methods/noop.bin and b/artifacts/test_program_methods/noop.bin differ diff --git a/artifacts/test_program_methods/program_owner_changer.bin b/artifacts/test_program_methods/program_owner_changer.bin index c50c0d9c..f4594460 100644 Binary files a/artifacts/test_program_methods/program_owner_changer.bin and b/artifacts/test_program_methods/program_owner_changer.bin differ diff --git a/artifacts/test_program_methods/simple_balance_transfer.bin b/artifacts/test_program_methods/simple_balance_transfer.bin index 0dbe37e7..b49e3007 100644 Binary files a/artifacts/test_program_methods/simple_balance_transfer.bin and b/artifacts/test_program_methods/simple_balance_transfer.bin differ diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 58b3fba6..035c502f 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -152,7 +152,7 @@ impl AccountPostState { } pub type BlockId = u64; -pub type ValidityRange = (Option , Option); +pub type ValidityWindow = (Option, Option); #[derive(Serialize, Deserialize, Clone)] #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] @@ -163,24 +163,48 @@ pub struct ProgramOutput { pub pre_states: Vec, pub post_states: Vec, pub chained_calls: Vec, - pub validity_range: ValidityRange, + pub validity_window: ValidityWindow, } impl ProgramOutput { + #[must_use] + pub const fn new( + instruction_data: InstructionData, + pre_states: Vec, + post_states: Vec, + ) -> Self { + Self { + instruction_data, + pre_states, + post_states, + chained_calls: Vec::new(), + validity_window: (None, None), + } + } + + pub fn write(self) { + env::commit(&self); + } + + #[must_use] + pub fn with_chained_calls(mut self, chained_calls: Vec) -> Self { + self.chained_calls = chained_calls; + self + } + #[must_use] pub const fn valid_from_id(mut self, id: BlockId) -> Self { - self.validity_range.0 = Some(id); + self.validity_window.0 = Some(id); self } #[must_use] pub const fn valid_until_id(mut self, id: BlockId) -> Self { - self.validity_range.1 = Some(id); + self.validity_window.1 = Some(id); self } } - /// Representation of a number as `lo + hi * 2^128`. #[derive(PartialEq, Eq)] struct WrappedBalanceSum { @@ -243,14 +267,7 @@ pub fn write_nssa_outputs( pre_states: Vec, post_states: Vec, ) { - let output = ProgramOutput { - instruction_data, - pre_states, - post_states, - chained_calls: Vec::new(), - validity_range: (None, None) - }; - env::commit(&output); + ProgramOutput::new(instruction_data, pre_states, post_states).write(); } pub fn write_nssa_outputs_with_chained_call( @@ -259,14 +276,9 @@ pub fn write_nssa_outputs_with_chained_call( post_states: Vec, chained_calls: Vec, ) { - let output = ProgramOutput { - instruction_data, - pre_states, - post_states, - chained_calls, - validity_range: (None, None) - }; - env::commit(&output); + ProgramOutput::new(instruction_data, pre_states, post_states) + .with_chained_calls(chained_calls) + .write(); } /// Validates well-behaved program execution. diff --git a/nssa/src/error.rs b/nssa/src/error.rs index 3576b366..15d4f044 100644 --- a/nssa/src/error.rs +++ b/nssa/src/error.rs @@ -69,6 +69,9 @@ pub enum NssaError { #[error("Max account nonce reached")] MaxAccountNonceReached, + + #[error("Execution outside of the validity window")] + OutOfValidityWindow, } #[cfg(test)] diff --git a/nssa/src/program.rs b/nssa/src/program.rs index 3b372a22..fa5e7b42 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -284,6 +284,14 @@ mod tests { // `program_methods` Self::new(MODIFIED_TRANSFER_ELF.to_vec()).unwrap() } + + #[must_use] + pub fn validity_window() -> Self { + use test_program_methods::VALIDITY_WINDOW_ELF; + // This unwrap won't panic since the `VALIDITY_WINDOW_ELF` comes from risc0 build of + // `program_methods` + Self::new(VALIDITY_WINDOW_ELF.to_vec()).unwrap() + } } #[test] diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index 8c84d83c..30d2e92f 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -4,7 +4,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use log::debug; use nssa_core::{ account::{Account, AccountId, AccountWithMetadata}, - program::{ChainedCall, DEFAULT_PROGRAM_ID, validate_execution}, + program::{BlockId, ChainedCall, DEFAULT_PROGRAM_ID, validate_execution}, }; use sha2::{Digest as _, digest::FixedOutput as _}; @@ -70,6 +70,7 @@ impl PublicTransaction { pub(crate) fn validate_and_produce_public_state_diff( &self, state: &V02State, + block_id: BlockId, ) -> Result, NssaError> { let message = self.message(); let witness_set = self.witness_set(); @@ -190,6 +191,14 @@ impl PublicTransaction { NssaError::InvalidProgramBehavior ); + // Verify validity window + if let Some(from_id) = program_output.validity_window.0 { + ensure!(from_id <= block_id, NssaError::OutOfValidityWindow); + } + if let Some(until_id) = program_output.validity_window.1 { + ensure!(until_id >= block_id, NssaError::OutOfValidityWindow); + } + for post in program_output .post_states .iter_mut() @@ -359,7 +368,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); + let result = tx.validate_and_produce_public_state_diff(&state, 1); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } @@ -379,7 +388,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); + let result = tx.validate_and_produce_public_state_diff(&state, 1); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } @@ -400,7 +409,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); + let result = tx.validate_and_produce_public_state_diff(&state, 1); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } @@ -420,7 +429,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); + let result = tx.validate_and_produce_public_state_diff(&state, 1); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } @@ -436,7 +445,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); + let result = tx.validate_and_produce_public_state_diff(&state, 1); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } } diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 9e9ef1e4..a3413466 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -2,7 +2,9 @@ use std::collections::{BTreeSet, HashMap, HashSet}; use borsh::{BorshDeserialize, BorshSerialize}; use nssa_core::{ - account::{Account, AccountId, Nonce}, program::{BlockId, ProgramId}, Commitment, CommitmentSetDigest, MembershipProof, Nullifier, DUMMY_COMMITMENT + Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, Nullifier, + account::{Account, AccountId, Nonce}, + program::{BlockId, ProgramId}, }; use crate::{ @@ -155,9 +157,9 @@ impl V02State { pub fn transition_from_public_transaction( &mut self, tx: &PublicTransaction, - _block_id: BlockId, + block_id: BlockId, ) -> Result<(), NssaError> { - let state_diff = tx.validate_and_produce_public_state_diff(self)?; + let state_diff = tx.validate_and_produce_public_state_diff(self, block_id)?; #[expect( clippy::iter_over_hash_type, @@ -338,7 +340,7 @@ pub mod tests { Commitment, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, encryption::{EphemeralPublicKey, Scalar, ViewingPublicKey}, - program::{PdaSeed, ProgramId}, + program::{BlockId, PdaSeed, ProgramId, ValidityWindow}, }; use crate::{ @@ -373,6 +375,7 @@ pub mod tests { self.insert_program(Program::amm()); self.insert_program(Program::claimer()); self.insert_program(Program::changer_claimer()); + self.insert_program(Program::validity_window()); self } @@ -2996,6 +2999,53 @@ pub mod tests { assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); } + #[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 validity_window_works(validity_window: ValidityWindow, block_id: BlockId) { + 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 = + V02State::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 message = public_transaction::Message::try_new( + program_id, + account_ids, + nonces, + validity_window, + ) + .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.0, validity_window.1) { + (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] fn state_serialization_roundtrip() { let account_id_1 = AccountId::new([1; 32]);