From d3827fc82342a012981b63920e1435464bc9cfa5 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Tue, 12 Aug 2025 23:31:41 -0300 Subject: [PATCH] add tests. refactor --- common/src/block.rs | 9 +- nssa/src/public_transaction/encoding.rs | 9 +- nssa/src/public_transaction/mod.rs | 146 +--------------- nssa/src/public_transaction/transaction.rs | 183 +++++++++++++++++++++ nssa/src/tests/mod.rs | 1 + nssa/src/tests/public_transaction_tests.rs | 28 ++++ 6 files changed, 224 insertions(+), 152 deletions(-) create mode 100644 nssa/src/public_transaction/transaction.rs create mode 100644 nssa/src/tests/public_transaction_tests.rs diff --git a/common/src/block.rs b/common/src/block.rs index 55490b4..2e4e9b9 100644 --- a/common/src/block.rs +++ b/common/src/block.rs @@ -65,6 +65,7 @@ impl HashableBlockData { bytes } + // TODO: Improve error handling. Remove unwraps. pub fn from_bytes(data: &[u8]) -> Self { let mut cursor = Cursor::new(data); @@ -91,12 +92,14 @@ impl HashableBlockData { } } +// TODO: Improve error handling. Remove unwraps. fn u32_from_cursor(cursor: &mut Cursor<&[u8]>) -> u32 { let mut word_buf = [0u8; 4]; cursor.read_exact(&mut word_buf).unwrap(); u32::from_le_bytes(word_buf) } +// TODO: Improve error handling. Remove unwraps. fn u64_from_cursor(cursor: &mut Cursor<&[u8]>) -> u64 { let mut word_buf = [0u8; 8]; cursor.read_exact(&mut word_buf).unwrap(); @@ -108,12 +111,12 @@ mod tests { use crate::{block::HashableBlockData, test_utils}; #[test] - fn test() { + fn test_encoding_roundtrip() { let transactions = vec![test_utils::produce_dummy_empty_transaction()]; let block = test_utils::produce_dummy_block(1, Some([1; 32]), transactions); let hashable = HashableBlockData::from(block); let bytes = hashable.to_bytes(); - let recov = HashableBlockData::from_bytes(&bytes); - assert_eq!(hashable, recov); + let block_from_bytes = HashableBlockData::from_bytes(&bytes); + assert_eq!(hashable, block_from_bytes); } } diff --git a/nssa/src/public_transaction/encoding.rs b/nssa/src/public_transaction/encoding.rs index 6d561ef..371f1b5 100644 --- a/nssa/src/public_transaction/encoding.rs +++ b/nssa/src/public_transaction/encoding.rs @@ -121,8 +121,8 @@ impl WitnessSet { impl PublicTransaction { pub fn to_bytes(&self) -> Vec { - let mut bytes = self.message.to_bytes(); - bytes.extend_from_slice(&self.witness_set.to_bytes()); + let mut bytes = self.message().to_bytes(); + bytes.extend_from_slice(&self.witness_set().to_bytes()); bytes } @@ -134,10 +134,7 @@ impl PublicTransaction { pub fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result { let message = Message::from_cursor(cursor)?; let witness_set = WitnessSet::from_cursor(cursor)?; - Ok(Self { - message, - witness_set, - }) + Ok(PublicTransaction::new(message, witness_set)) } } diff --git a/nssa/src/public_transaction/mod.rs b/nssa/src/public_transaction/mod.rs index 44eee56..ce9ae9e 100644 --- a/nssa/src/public_transaction/mod.rs +++ b/nssa/src/public_transaction/mod.rs @@ -1,153 +1,13 @@ -use std::collections::{HashMap, HashSet}; - -use nssa_core::{ - account::{Account, AccountWithMetadata}, - program::validate_execution, -}; -use sha2::{Digest, digest::FixedOutput}; - -use crate::{V01State, address::Address, error::NssaError}; mod encoding; mod message; mod witness_set; +mod transaction; pub use message::Message; pub use witness_set::WitnessSet; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PublicTransaction { - message: Message, - witness_set: WitnessSet, -} - -impl PublicTransaction { - pub fn message(&self) -> &Message { - &self.message - } - - pub fn witness_set(&self) -> &WitnessSet { - &self.witness_set - } - - pub(crate) fn signer_addresses(&self) -> Vec
{ - self.witness_set - .signatures_and_public_keys - .iter() - .map(|(_, public_key)| Address::from_public_key(public_key)) - .collect() - } - - pub fn new(message: Message, witness_set: WitnessSet) -> Self { - Self { - message, - witness_set, - } - } - - pub fn hash(&self) -> [u8; 32] { - let bytes = self.to_bytes(); - let mut hasher = sha2::Sha256::new(); - hasher.update(&bytes); - hasher.finalize_fixed().into() - } - - pub(crate) fn validate_and_compute_post_states( - &self, - state: &V01State, - ) -> Result, NssaError> { - let message = self.message(); - let witness_set = self.witness_set(); - - // All addresses must be different - if message.addresses.iter().collect::>().len() != message.addresses.len() { - return Err(NssaError::InvalidInput( - "Duplicate addresses found in message".into(), - )); - } - - if message.nonces.len() != witness_set.signatures_and_public_keys.len() { - return Err(NssaError::InvalidInput( - "Mismatch between number of nonces and signatures/public keys".into(), - )); - } - - // Check the signatures are valid - if !witness_set.is_valid_for(message) { - return Err(NssaError::InvalidInput( - "Invalid signature for given message and public key".into(), - )); - } - - let signer_addresses = self.signer_addresses(); - // Check nonces corresponds to the current nonces on the public state. - for (address, nonce) in signer_addresses.iter().zip(&message.nonces) { - let current_nonce = state.get_account_by_address(address).nonce; - if current_nonce != *nonce { - return Err(NssaError::InvalidInput("Nonce mismatch".into())); - } - } - - // Build pre_states for execution - let pre_states: Vec<_> = message - .addresses - .iter() - .map(|address| AccountWithMetadata { - account: state.get_account_by_address(address), - is_authorized: signer_addresses.contains(address), - }) - .collect(); - - // Check the `program_id` corresponds to a built-in program - // Only allowed program so far is the authenticated transfer program - let Some(program) = state.builtin_programs().get(&message.program_id) else { - return Err(NssaError::InvalidInput("Unknown program".into())); - }; - - // // Execute program - let post_states = program.execute(&pre_states, &message.instruction_data)?; - - // Verify execution corresponds to a well-behaved program. - // See the # Programs section for the definition of the `validate_execution` method. - if !validate_execution(&pre_states, &post_states, message.program_id) { - return Err(NssaError::InvalidProgramBehavior); - } - - Ok(message.addresses.iter().cloned().zip(post_states).collect()) - } -} +pub use transaction::PublicTransaction; #[cfg(test)] -mod tests { - use std::io::Cursor; +pub use transaction::tests; - use crate::{ - Address, PrivateKey, PublicKey, PublicTransaction, - program::Program, - public_transaction::{Message, WitnessSet}, - }; - - #[test] - fn test_public_transaction_encoding_bytes_roundtrip() { - let key1 = PrivateKey::try_new([1; 32]).unwrap(); - let key2 = PrivateKey::try_new([2; 32]).unwrap(); - let addr1 = Address::from_public_key(&PublicKey::new(&key1)); - let addr2 = Address::from_public_key(&PublicKey::new(&key2)); - let nonces = vec![5, 99]; - let instruction = 1337; - let message = Message::try_new( - Program::authenticated_transfer_program().id(), - vec![addr1, addr2], - nonces, - instruction, - ) - .unwrap(); - - let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]); - let tx = PublicTransaction::new(message, witness_set); - - let bytes = tx.to_bytes(); - let tx_from_bytes = PublicTransaction::from_bytes(&bytes).unwrap(); - assert_eq!(tx, tx_from_bytes); - } -} diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs new file mode 100644 index 0000000..8d6bc47 --- /dev/null +++ b/nssa/src/public_transaction/transaction.rs @@ -0,0 +1,183 @@ +use std::collections::{HashMap, HashSet}; + +use nssa_core::{ + account::{Account, AccountWithMetadata}, + program::validate_execution, +}; +use sha2::{Digest, digest::FixedOutput}; + +use crate::{ + V01State, + address::Address, + error::NssaError, + public_transaction::{Message, WitnessSet}, +}; + + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PublicTransaction { + message: Message, + witness_set: WitnessSet, +} + +impl PublicTransaction { + pub fn new(message: Message, witness_set: WitnessSet) -> Self { + Self { + message, + witness_set, + } + } + + pub fn message(&self) -> &Message { + &self.message + } + + pub fn witness_set(&self) -> &WitnessSet { + &self.witness_set + } + + pub(crate) fn signer_addresses(&self) -> Vec
{ + self.witness_set + .iter_signatures() + .map(|(_, public_key)| Address::from_public_key(public_key)) + .collect() + } + + pub fn hash(&self) -> [u8; 32] { + let bytes = self.to_bytes(); + let mut hasher = sha2::Sha256::new(); + hasher.update(&bytes); + hasher.finalize_fixed().into() + } + + pub(crate) fn validate_and_compute_post_states( + &self, + state: &V01State, + ) -> Result, NssaError> { + let message = self.message(); + let witness_set = self.witness_set(); + + // All addresses must be different + if message.addresses.iter().collect::>().len() != message.addresses.len() { + return Err(NssaError::InvalidInput( + "Duplicate addresses found in message".into(), + )); + } + + if message.nonces.len() != witness_set.signatures_and_public_keys.len() { + return Err(NssaError::InvalidInput( + "Mismatch between number of nonces and signatures/public keys".into(), + )); + } + + // Check the signatures are valid + if !witness_set.is_valid_for(message) { + return Err(NssaError::InvalidInput( + "Invalid signature for given message and public key".into(), + )); + } + + let signer_addresses = self.signer_addresses(); + // Check nonces corresponds to the current nonces on the public state. + for (address, nonce) in signer_addresses.iter().zip(&message.nonces) { + let current_nonce = state.get_account_by_address(address).nonce; + if current_nonce != *nonce { + return Err(NssaError::InvalidInput("Nonce mismatch".into())); + } + } + + // Build pre_states for execution + let pre_states: Vec<_> = message + .addresses + .iter() + .map(|address| AccountWithMetadata { + account: state.get_account_by_address(address), + is_authorized: signer_addresses.contains(address), + }) + .collect(); + + // Check the `program_id` corresponds to a built-in program + // Only allowed program so far is the authenticated transfer program + let Some(program) = state.builtin_programs().get(&message.program_id) else { + return Err(NssaError::InvalidInput("Unknown program".into())); + }; + + // // Execute program + let post_states = program.execute(&pre_states, &message.instruction_data)?; + + // Verify execution corresponds to a well-behaved program. + // See the # Programs section for the definition of the `validate_execution` method. + if !validate_execution(&pre_states, &post_states, message.program_id) { + return Err(NssaError::InvalidProgramBehavior); + } + + Ok(message.addresses.iter().cloned().zip(post_states).collect()) + } +} + +#[cfg(test)] +pub mod tests { + use crate::{ + Address, PrivateKey, PublicKey, PublicTransaction, + program::Program, + public_transaction::{Message, WitnessSet, witness_set}, + }; + + pub fn transaction_for_tests() -> PublicTransaction { + let key1 = PrivateKey::try_new([1; 32]).unwrap(); + let key2 = PrivateKey::try_new([2; 32]).unwrap(); + let addr1 = Address::from_public_key(&PublicKey::new(&key1)); + let addr2 = Address::from_public_key(&PublicKey::new(&key2)); + let nonces = vec![5, 99]; + let instruction = 1337; + let message = Message::try_new( + Program::authenticated_transfer_program().id(), + vec![addr1, addr2], + nonces, + instruction, + ) + .unwrap(); + + let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]); + PublicTransaction::new(message, witness_set) + } + + #[test] + fn test_new_constructor() { + let tx = transaction_for_tests(); + let message = tx.message().clone(); + let witness_set = tx.witness_set().clone(); + let tx_from_constructor = PublicTransaction::new(message.clone(), witness_set.clone()); + assert_eq!(tx_from_constructor.message, message); + assert_eq!(tx_from_constructor.witness_set, witness_set); + } + + #[test] + fn test_message_getter() { + let tx = transaction_for_tests(); + assert_eq!(&tx.message, tx.message()); + } + + #[test] + fn test_witness_set_getter() { + let tx = transaction_for_tests(); + assert_eq!(&tx.witness_set, tx.witness_set()); + } + + #[test] + fn test_signer_addresses() { + let tx = transaction_for_tests(); + let expected_signer_addresses = vec![ + Address::new([ + 27, 132, 197, 86, 123, 18, 100, 64, 153, 93, 62, 213, 170, 186, 5, 101, 215, 30, + 24, 52, 96, 72, 25, 255, 156, 23, 245, 233, 213, 221, 7, 143, + ]), + Address::new([ + 77, 75, 108, 209, 54, 16, 50, 202, 155, 210, 174, 185, 217, 0, 170, 77, 69, 217, + 234, 216, 10, 201, 66, 51, 116, 196, 81, 167, 37, 77, 7, 102, + ]), + ]; + let signer_addresses = tx.signer_addresses(); + assert_eq!(signer_addresses, expected_signer_addresses); + } +} diff --git a/nssa/src/tests/mod.rs b/nssa/src/tests/mod.rs index 70ae5bd..46fd6f8 100644 --- a/nssa/src/tests/mod.rs +++ b/nssa/src/tests/mod.rs @@ -3,3 +3,4 @@ mod program_tests; mod signature_tests; mod state_tests; mod valid_execution_tests; +mod public_transaction_tests; diff --git a/nssa/src/tests/public_transaction_tests.rs b/nssa/src/tests/public_transaction_tests.rs new file mode 100644 index 0000000..3401857 --- /dev/null +++ b/nssa/src/tests/public_transaction_tests.rs @@ -0,0 +1,28 @@ +use sha2::{Digest, digest::FixedOutput}; + +use crate::{ + Address, PrivateKey, PublicKey, PublicTransaction, + program::Program, + public_transaction::{Message, WitnessSet, tests::transaction_for_tests}, +}; + +#[test] +fn test_public_transaction_encoding_bytes_roundtrip() { + let tx = transaction_for_tests(); + let bytes = tx.to_bytes(); + let tx_from_bytes = PublicTransaction::from_bytes(&bytes).unwrap(); + assert_eq!(tx, tx_from_bytes); +} + +#[test] +fn test_hash_is_sha256_of_transaction_bytes() { + let tx = transaction_for_tests(); + let hash = tx.hash(); + let expected_hash: [u8; 32] = { + let bytes = tx.to_bytes(); + let mut hasher = sha2::Sha256::new(); + hasher.update(&bytes); + hasher.finalize_fixed().into() + }; + assert_eq!(hash, expected_hash); +}