diff --git a/artifacts/program_methods/privacy_preserving_circuit.bin b/artifacts/program_methods/privacy_preserving_circuit.bin index 8e0097f4..cede905d 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/test_program_methods/validity_window.bin b/artifacts/test_program_methods/validity_window.bin new file mode 100644 index 00000000..1ea3d759 Binary files /dev/null and b/artifacts/test_program_methods/validity_window.bin differ diff --git a/indexer/service/protocol/src/convert.rs b/indexer/service/protocol/src/convert.rs index 499baa4c..a53da4ee 100644 --- a/indexer/service/protocol/src/convert.rs +++ b/indexer/service/protocol/src/convert.rs @@ -3,11 +3,7 @@ use nssa_core::account::Nonce; use crate::{ - Account, AccountId, BedrockStatus, Block, BlockBody, BlockHeader, Ciphertext, Commitment, - CommitmentSetDigest, Data, EncryptedAccountData, EphemeralPublicKey, HashType, MantleMsgId, - Nullifier, PrivacyPreservingMessage, PrivacyPreservingTransaction, ProgramDeploymentMessage, - ProgramDeploymentTransaction, ProgramId, Proof, PublicKey, PublicMessage, PublicTransaction, - Signature, Transaction, WitnessSet, + Account, AccountId, BedrockStatus, Block, BlockBody, BlockHeader, Ciphertext, Commitment, CommitmentSetDigest, Data, EncryptedAccountData, EphemeralPublicKey, HashType, MantleMsgId, Nullifier, PrivacyPreservingMessage, PrivacyPreservingTransaction, ProgramDeploymentMessage, ProgramDeploymentTransaction, ProgramId, Proof, PublicKey, PublicMessage, PublicTransaction, Signature, Transaction, ValidityWindow, WitnessSet }; // ============================================================================ @@ -287,6 +283,7 @@ impl From for PrivacyPre encrypted_private_post_states, new_commitments, new_nullifiers, + validity_window, } = value; Self { public_account_ids: public_account_ids.into_iter().map(Into::into).collect(), @@ -301,6 +298,7 @@ impl From for PrivacyPre .into_iter() .map(|(n, d)| (n.into(), d.into())) .collect(), + validity_window: ValidityWindow(validity_window), } } } @@ -316,6 +314,7 @@ impl TryFrom for nssa::privacy_preserving_transaction: encrypted_private_post_states, new_commitments, new_nullifiers, + validity_window, } = value; Ok(Self { public_account_ids: public_account_ids.into_iter().map(Into::into).collect(), @@ -336,6 +335,7 @@ impl TryFrom for nssa::privacy_preserving_transaction: .into_iter() .map(|(n, d)| (n.into(), d.into())) .collect(), + validity_window: validity_window.0, }) } } diff --git a/indexer/service/protocol/src/lib.rs b/indexer/service/protocol/src/lib.rs index 98ef5650..6015c5aa 100644 --- a/indexer/service/protocol/src/lib.rs +++ b/indexer/service/protocol/src/lib.rs @@ -235,6 +235,7 @@ pub struct PrivacyPreservingMessage { pub encrypted_private_post_states: Vec, pub new_commitments: Vec, pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>, + pub validity_window: ValidityWindow, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] @@ -300,6 +301,9 @@ pub struct Nullifier( pub [u8; 32], ); +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] +pub struct ValidityWindow(pub (Option, Option)); + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] pub struct CommitmentSetDigest( #[serde(with = "base64::arr")] diff --git a/nssa/core/src/circuit_io.rs b/nssa/core/src/circuit_io.rs index 56d63022..d187f359 100644 --- a/nssa/core/src/circuit_io.rs +++ b/nssa/core/src/circuit_io.rs @@ -1,11 +1,7 @@ use serde::{Deserialize, Serialize}; use crate::{ - Commitment, CommitmentSetDigest, MembershipProof, Nullifier, NullifierPublicKey, - NullifierSecretKey, SharedSecretKey, - account::{Account, AccountWithMetadata}, - encryption::Ciphertext, - program::{ProgramId, ProgramOutput}, + account::{Account, AccountWithMetadata}, encryption::Ciphertext, program::{ProgramId, ProgramOutput, ValidityWindow}, Commitment, CommitmentSetDigest, MembershipProof, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey }; #[derive(Serialize, Deserialize)] @@ -36,6 +32,7 @@ pub struct PrivacyPreservingCircuitOutput { pub ciphertexts: Vec, pub new_commitments: Vec, pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>, + pub validity_window: ValidityWindow } #[cfg(feature = "host")] @@ -101,6 +98,7 @@ mod tests { ), [0xab; 32], )], + validity_window: (Some(1), None), }; let bytes = output.to_bytes(); let output_from_slice: PrivacyPreservingCircuitOutput = from_slice(&bytes).unwrap(); diff --git a/nssa/src/privacy_preserving_transaction/message.rs b/nssa/src/privacy_preserving_transaction/message.rs index 4b93e820..a79b1ffa 100644 --- a/nssa/src/privacy_preserving_transaction/message.rs +++ b/nssa/src/privacy_preserving_transaction/message.rs @@ -3,6 +3,7 @@ use nssa_core::{ Commitment, CommitmentSetDigest, Nullifier, NullifierPublicKey, PrivacyPreservingCircuitOutput, account::{Account, Nonce}, encryption::{Ciphertext, EphemeralPublicKey, ViewingPublicKey}, + program::ValidityWindow, }; use sha2::{Digest as _, Sha256}; @@ -52,6 +53,7 @@ pub struct Message { pub encrypted_private_post_states: Vec, pub new_commitments: Vec, pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>, + pub validity_window: ValidityWindow, } impl std::fmt::Debug for Message { @@ -77,6 +79,7 @@ impl std::fmt::Debug for Message { ) .field("new_commitments", &self.new_commitments) .field("new_nullifiers", &nullifiers) + .field("validity_window", &self.validity_window) .finish() } } @@ -109,6 +112,7 @@ impl Message { encrypted_private_post_states, new_commitments: output.new_commitments, new_nullifiers: output.new_nullifiers, + validity_window: output.validity_window, }) } } @@ -161,6 +165,7 @@ pub mod tests { encrypted_private_post_states, new_commitments, new_nullifiers, + validity_window: (None, None), } } diff --git a/nssa/src/privacy_preserving_transaction/transaction.rs b/nssa/src/privacy_preserving_transaction/transaction.rs index 2b268c07..7db6bdb2 100644 --- a/nssa/src/privacy_preserving_transaction/transaction.rs +++ b/nssa/src/privacy_preserving_transaction/transaction.rs @@ -7,6 +7,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use nssa_core::{ Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput, account::{Account, AccountWithMetadata}, + program::{BlockId, ValidityWindow}, }; use sha2::{Digest as _, digest::FixedOutput as _}; @@ -35,6 +36,7 @@ impl PrivacyPreservingTransaction { 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; @@ -112,6 +114,7 @@ impl PrivacyPreservingTransaction { &message.encrypted_private_post_states, &message.new_commitments, &message.new_nullifiers, + &message.validity_window, )?; // 5. Commitment freshness @@ -120,6 +123,18 @@ impl PrivacyPreservingTransaction { // 6. Nullifier uniqueness state.check_nullifiers_are_valid(&message.new_nullifiers)?; + // 7. Verify validity window + if let Some(from_id) = message.validity_window.0 + && block_id < from_id + { + return Err(NssaError::OutOfValidityWindow); + } + if let Some(until_id) = message.validity_window.1 + && until_id < block_id + { + return Err(NssaError::OutOfValidityWindow); + } + Ok(message .public_account_ids .iter() @@ -173,6 +188,7 @@ fn check_privacy_preserving_circuit_proof_is_valid( encrypted_private_post_states: &[EncryptedAccountData], new_commitments: &[Commitment], new_nullifiers: &[(Nullifier, CommitmentSetDigest)], + validity_window: &ValidityWindow, ) -> Result<(), NssaError> { let output = PrivacyPreservingCircuitOutput { public_pre_states: public_pre_states.to_vec(), @@ -184,6 +200,7 @@ fn check_privacy_preserving_circuit_proof_is_valid( .collect(), new_commitments: new_commitments.to_vec(), new_nullifiers: new_nullifiers.to_vec(), + validity_window: validity_window.to_owned(), }; proof .is_valid_for(&output) diff --git a/nssa/src/state.rs b/nssa/src/state.rs index a3413466..ba372879 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -182,10 +182,10 @@ impl V02State { pub fn transition_from_privacy_preserving_transaction( &mut self, tx: &PrivacyPreservingTransaction, - _block_id: BlockId, + block_id: BlockId, ) -> Result<(), NssaError> { // 1. Verify the transaction satisfies acceptance criteria - let public_state_diff = tx.validate_and_produce_public_state_diff(self)?; + let public_state_diff = tx.validate_and_produce_public_state_diff(self, block_id)?; let message = tx.message(); @@ -3012,12 +3012,14 @@ pub mod tests { #[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) { + fn validity_window_works_in_public_transactions( + 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 mut state = V02State::new_with_genesis_accounts(&[], &[]).with_test_programs(); let tx = { let account_ids = vec![pre.account_id]; let nonces = vec![]; @@ -3046,6 +3048,68 @@ 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 validity_window_works_in_privacy_preserving_transactions( + validity_window: ValidityWindow, + block_id: BlockId, + ) { + 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 = V02State::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 (output, proof) = circuit::execute_and_prove( + vec![pre], + Program::serialize_instruction(validity_window).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, 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]); diff --git a/program_methods/guest/src/bin/privacy_preserving_circuit.rs b/program_methods/guest/src/bin/privacy_preserving_circuit.rs index 99782d7f..c66fbee9 100644 --- a/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -11,7 +11,7 @@ use nssa_core::{ compute_digest_for_path, program::{ AccountPostState, ChainedCall, DEFAULT_PROGRAM_ID, MAX_NUMBER_CHAINED_CALLS, ProgramId, - ProgramOutput, validate_execution, + ProgramOutput, ValidityWindow, validate_execution, }, }; use risc0_zkvm::{guest::env, serde::to_vec}; @@ -20,11 +20,27 @@ use risc0_zkvm::{guest::env, serde::to_vec}; struct ExecutionState { pre_states: Vec, post_states: HashMap, + validity_window: ValidityWindow, } impl ExecutionState { /// Validate program outputs and derive the overall execution state. pub fn derive_from_outputs(program_id: ProgramId, program_outputs: Vec) -> Self { + let valid_from_id = program_outputs + .iter() + .filter_map(|output| output.validity_window.0) + .max(); + let valid_until_id = program_outputs + .iter() + .filter_map(|output| output.validity_window.1) + .min(); + + let mut execution_state = Self { + pre_states: Vec::new(), + post_states: HashMap::new(), + validity_window: (valid_from_id, valid_until_id), + }; + let Some(first_output) = program_outputs.first() else { panic!("No program outputs provided"); }; @@ -37,10 +53,6 @@ impl ExecutionState { }; let mut chained_calls = VecDeque::from_iter([(initial_call, None)]); - let mut execution_state = Self { - pre_states: Vec::new(), - post_states: HashMap::new(), - }; let mut program_outputs_iter = program_outputs.into_iter(); let mut chain_calls_counter = 0; @@ -210,6 +222,7 @@ fn compute_circuit_output( ciphertexts: Vec::new(), new_commitments: Vec::new(), new_nullifiers: Vec::new(), + validity_window: execution_state.validity_window, }; let states_iter = execution_state.into_states_iter(); diff --git a/test_program_methods/guest/src/bin/validity_window.rs b/test_program_methods/guest/src/bin/validity_window.rs new file mode 100644 index 00000000..dbea9849 --- /dev/null +++ b/test_program_methods/guest/src/bin/validity_window.rs @@ -0,0 +1,36 @@ +use nssa_core::program::{ + AccountPostState, BlockId, ProgramInput, ProgramOutput, read_nssa_inputs, +}; + +type Instruction = (Option, Option); + +fn main() { + let ( + ProgramInput { + pre_states, + instruction: (from_id, until_id), + }, + instruction_words, + ) = read_nssa_inputs::(); + + let Ok([pre]) = <[_; 1]>::try_from(pre_states) else { + return; + }; + + let post = pre.account.clone(); + + let mut output = ProgramOutput::new( + instruction_words, + vec![pre], + vec![AccountPostState::new(post)], + ); + + if let Some(id) = from_id { + output = output.valid_from_id(id); + } + if let Some(id) = until_id { + output = output.valid_until_id(id); + } + + output.write(); +}