diff --git a/nssa/src/state.rs b/nssa/src/state.rs index e250356c..5bf3cb99 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -344,7 +344,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::{ @@ -3021,7 +3021,7 @@ pub mod tests { validity_window: (Option, Option), block_id: BlockId, ) { - let validity_window: ValidityWindow = validity_window.try_into().unwrap(); + let block_validity_window: ValidityWindow = 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()); @@ -3030,7 +3030,7 @@ pub mod tests { let account_ids = vec![pre.account_id]; let nonces = vec![]; let program_id = validity_window_program.id(); - let instruction = validity_window; + let instruction = (block_validity_window, ValidityWindow::::new_unbounded()); let message = public_transaction::Message::try_new(program_id, account_ids, nonces, instruction) .unwrap(); @@ -3038,7 +3038,7 @@ pub mod tests { PublicTransaction::new(message, witness_set) }; let result = state.transition_from_public_transaction(&tx, block_id, 0); - let is_inside_validity_window = match (validity_window.start(), validity_window.end()) { + 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, @@ -3051,6 +3051,56 @@ pub mod tests { } } + #[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, Option), + timestamp_ms: Timestamp, + ) { + let timestamp_validity_window: ValidityWindow = + 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 = + (ValidityWindow::::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_ms); + let is_inside_validity_window = + match (timestamp_validity_window.start(), timestamp_validity_window.end()) { + (Some(s), Some(e)) => s <= timestamp_ms && timestamp_ms < e, + (Some(s), None) => s <= timestamp_ms, + (None, Some(e)) => timestamp_ms < 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")] @@ -3068,7 +3118,7 @@ pub mod tests { validity_window: (Option, Option), block_id: BlockId, ) { - let validity_window: ValidityWindow = validity_window.try_into().unwrap(); + let block_validity_window: ValidityWindow = 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()); @@ -3078,7 +3128,7 @@ pub mod tests { let shared_secret = SharedSecretKey::new(&esk, &account_keys.vpk()); let epk = EphemeralPublicKey::from_scalar(esk); - let instruction = validity_window; + let instruction = (block_validity_window, ValidityWindow::::new_unbounded()); let (output, proof) = circuit::execute_and_prove( vec![pre], Program::serialize_instruction(instruction).unwrap(), @@ -3102,12 +3152,80 @@ pub mod tests { PrivacyPreservingTransaction::new(message, witness_set) }; 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, - (None, Some(e)) => block_id < e, - (None, None) => true, + 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, Option), + timestamp_ms: Timestamp, + ) { + let timestamp_validity_window: ValidityWindow = + 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 = + (ValidityWindow::::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_ms); + let is_inside_validity_window = + match (timestamp_validity_window.start(), timestamp_validity_window.end()) { + (Some(s), Some(e)) => s <= timestamp_ms && timestamp_ms < e, + (Some(s), None) => s <= timestamp_ms, + (None, Some(e)) => timestamp_ms < e, + (None, None) => true, + }; if is_inside_validity_window { assert!(result.is_ok()); } else { diff --git a/test_program_methods/guest/src/bin/validity_window.rs b/test_program_methods/guest/src/bin/validity_window.rs index d2747401..71237055 100644 --- a/test_program_methods/guest/src/bin/validity_window.rs +++ b/test_program_methods/guest/src/bin/validity_window.rs @@ -1,14 +1,15 @@ use nssa_core::program::{ - AccountPostState, BlockId, ProgramInput, ProgramOutput, ValidityWindow, read_nssa_inputs, + AccountPostState, BlockId, ProgramInput, ProgramOutput, Timestamp, ValidityWindow, + read_nssa_inputs, }; -type Instruction = ValidityWindow; +type Instruction = (ValidityWindow, ValidityWindow); fn main() { let ( ProgramInput { pre_states, - instruction: block_validity_window, + instruction: (block_validity_window, timestamp_validity_window), }, instruction_words, ) = read_nssa_inputs::(); @@ -25,5 +26,6 @@ fn main() { vec![AccountPostState::new(post)], ) .with_block_validity_window(block_validity_window) + .with_timestamp_validity_window(timestamp_validity_window) .write(); } diff --git a/test_program_methods/guest/src/bin/validity_window_chain_caller.rs b/test_program_methods/guest/src/bin/validity_window_chain_caller.rs index 796469cd..1a4a1d2c 100644 --- a/test_program_methods/guest/src/bin/validity_window_chain_caller.rs +++ b/test_program_methods/guest/src/bin/validity_window_chain_caller.rs @@ -1,6 +1,6 @@ use nssa_core::program::{ - AccountPostState, BlockId, ChainedCall, ProgramId, ProgramInput, ProgramOutput, ValidityWindow, - read_nssa_inputs, + AccountPostState, BlockId, ChainedCall, ProgramId, ProgramInput, ProgramOutput, Timestamp, + ValidityWindow, read_nssa_inputs, }; use risc0_zkvm::serde::to_vec; @@ -9,6 +9,8 @@ use risc0_zkvm::serde::to_vec; /// /// Instruction: (`window`, `chained_program_id`, `chained_window`) /// The initial output uses `window` and chains to `chained_program_id` with `chained_window`. +/// The chained program (validity_window) expects `(ValidityWindow, ValidityWindow)` +/// so an unbounded timestamp window is appended automatically. type Instruction = (ValidityWindow, ProgramId, ValidityWindow); fn main() { @@ -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_block_validity_window).unwrap(); + let chained_instruction = to_vec(&( + chained_block_validity_window, + ValidityWindow::::new_unbounded(), + )) + .unwrap(); let chained_call = ChainedCall { program_id: chained_program_id, instruction_data: chained_instruction,