From a185e522032b5942e1fdfe54e05be7f652e60c9d Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Mon, 18 Aug 2025 14:28:26 -0300 Subject: [PATCH] add encodings --- nssa/Cargo.toml | 2 +- nssa/core/Cargo.toml | 5 + nssa/core/src/account/commitment.rs | 16 +- nssa/core/src/account/encoding.rs | 141 ++++++++++++++++-- nssa/core/src/account/nullifier.rs | 10 +- nssa/core/src/error.rs | 12 ++ nssa/core/src/lib.rs | 20 +++ nssa/src/error.rs | 5 + .../encoding.rs | 140 +++++++++++++++++ .../privacy_preserving_transaction/message.rs | 2 +- .../src/privacy_preserving_transaction/mod.rs | 1 + .../transaction.rs | 5 + .../witness_set.rs | 2 +- nssa/src/public_transaction/encoding.rs | 12 +- nssa/src/state.rs | 22 ++- 15 files changed, 366 insertions(+), 29 deletions(-) create mode 100644 nssa/core/src/error.rs create mode 100644 nssa/src/privacy_preserving_transaction/encoding.rs diff --git a/nssa/Cargo.toml b/nssa/Cargo.toml index fef52efd..19e751a6 100644 --- a/nssa/Cargo.toml +++ b/nssa/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" [dependencies] thiserror = "2.0.12" risc0-zkvm = "2.3.1" -nssa-core = { path = "core" } +nssa-core = { path = "core", features=["host"]} program-methods = { path = "program_methods" } serde = "1.0.219" sha2 = "0.10.9" diff --git a/nssa/core/Cargo.toml b/nssa/core/Cargo.toml index feb907dd..e74eb907 100644 --- a/nssa/core/Cargo.toml +++ b/nssa/core/Cargo.toml @@ -6,3 +6,8 @@ edition = "2024" [dependencies] risc0-zkvm = "2.3.1" serde = { version = "1.0", default-features = false } +thiserror = { version = "2.0.12", optional = true } + +[features] +default = [] +host = ["thiserror"] diff --git a/nssa/core/src/account/commitment.rs b/nssa/core/src/account/commitment.rs index 9a3fc20c..60034cb5 100644 --- a/nssa/core/src/account/commitment.rs +++ b/nssa/core/src/account/commitment.rs @@ -13,7 +13,21 @@ impl Commitment { pub fn new(Npk: &NullifierPublicKey, account: &Account) -> Self { let mut bytes = Vec::new(); bytes.extend_from_slice(&Npk.to_byte_array()); - bytes.extend_from_slice(&account.to_bytes()); + let account_bytes_with_hashed_data = { + let mut this = Vec::new(); + for word in &account.program_owner { + this.extend_from_slice(&word.to_le_bytes()); + } + this.extend_from_slice(&account.balance.to_le_bytes()); + this.extend_from_slice(&account.nonce.to_le_bytes()); + let hashed_data: [u8; 32] = Impl::hash_bytes(&account.data) + .as_bytes() + .try_into() + .unwrap(); + this.extend_from_slice(&hashed_data); + this + }; + bytes.extend_from_slice(&account_bytes_with_hashed_data); Self(Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap()) } } diff --git a/nssa/core/src/account/encoding.rs b/nssa/core/src/account/encoding.rs index 91737b6d..ae96ebe8 100644 --- a/nssa/core/src/account/encoding.rs +++ b/nssa/core/src/account/encoding.rs @@ -1,9 +1,14 @@ -use risc0_zkvm::{ - serde::to_vec, - sha::{Impl, Sha256}, -}; +use risc0_zkvm::sha::{Impl, Sha256}; -use crate::account::{Account, Commitment, NullifierPublicKey}; +#[cfg(feature = "host")] +use std::io::Cursor; + +#[cfg(feature = "host")] +use std::io::Read; + +use crate::account::{Account, Commitment, Nullifier, NullifierPublicKey}; +#[cfg(feature = "host")] +use crate::error::NssaCoreError; impl Account { pub fn to_bytes(&self) -> Vec { @@ -13,28 +18,82 @@ impl Account { } bytes.extend_from_slice(&self.balance.to_le_bytes()); bytes.extend_from_slice(&self.nonce.to_le_bytes()); - let hashed_data: [u8; 32] = Impl::hash_bytes(&self.data).as_bytes().try_into().unwrap(); - bytes.extend_from_slice(&hashed_data); + let data_length: u32 = self.data.len() as u32; + bytes.extend_from_slice(&data_length.to_le_bytes()); + bytes.extend_from_slice(self.data.as_slice()); bytes } + + #[cfg(feature = "host")] + pub fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result { + let mut u32_bytes = [0u8; 4]; + let mut u128_bytes = [0u8; 16]; + + // program owner + let mut program_owner = [0u32; 8]; + for word in &mut program_owner { + cursor.read_exact(&mut u32_bytes)?; + *word = u32::from_le_bytes(u32_bytes); + } + + // balance + cursor.read_exact(&mut u128_bytes)?; + let balance = u128::from_le_bytes(u128_bytes); + + // nonce + cursor.read_exact(&mut u128_bytes)?; + let nonce = u128::from_le_bytes(u128_bytes); + + //data + cursor.read_exact(&mut u32_bytes)?; + let data_length = u32::from_le_bytes(u32_bytes); + let mut data = vec![0; data_length as usize]; + cursor.read_exact(&mut data)?; + + Ok(Self { + program_owner, + balance, + data, + nonce, + }) + } } impl Commitment { - pub(crate) fn to_byte_array(&self) -> [u8; 32] { + pub fn to_byte_array(&self) -> [u8; 32] { + self.0 + } + + #[cfg(feature = "host")] + pub fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result { + let mut bytes = [0u8; 32]; + cursor.read_exact(&mut bytes)?; + Ok(Self(bytes)) + } +} + +impl NullifierPublicKey { + pub fn to_byte_array(&self) -> [u8; 32] { self.0 } } - -impl NullifierPublicKey { - pub(crate) fn to_byte_array(&self) -> [u8; 32] { +#[cfg(feature = "host")] +impl Nullifier { + pub fn to_byte_array(&self) -> [u8; 32] { self.0 } + + pub fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result { + let mut bytes = [0u8; 32]; + cursor.read_exact(&mut bytes)?; + Ok(Self(bytes)) + } } #[cfg(test)] mod tests { - use crate::account::Account; + use super::*; #[test] fn test_enconding() { @@ -49,12 +108,64 @@ mod tests { let expected_bytes = [ 1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 192, 186, 220, 114, 113, 65, 236, 234, 222, 15, 215, 191, 227, 198, 23, 0, 42, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 137, 65, 102, 211, 51, 100, 53, 200, - 0, 190, 163, 111, 242, 27, 41, 234, 168, 1, 165, 47, 88, 76, 0, 108, 73, 40, 154, 13, - 207, 110, 47, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 104, 111, 108, 97, 32, 109, + 117, 110, 100, 111, ]; let bytes = account.to_bytes(); assert_eq!(bytes, expected_bytes); } + + #[test] + fn test_commitment_to_bytes() { + let commitment = Commitment((0..32).collect::>().try_into().unwrap()); + let expected_bytes: [u8; 32] = (0..32).collect::>().try_into().unwrap(); + + let bytes = commitment.to_byte_array(); + assert_eq!(expected_bytes, bytes); + } + + #[test] + fn test_nullifier_to_bytes() { + let nullifier = Nullifier((0..32).collect::>().try_into().unwrap()); + let expected_bytes: [u8; 32] = (0..32).collect::>().try_into().unwrap(); + + let bytes = nullifier.to_byte_array(); + assert_eq!(expected_bytes, bytes); + } + + #[cfg(feature = "host")] + #[test] + fn test_commitment_to_bytes_roundtrip() { + let commitment = Commitment((0..32).collect::>().try_into().unwrap()); + let bytes = commitment.to_byte_array(); + let mut cursor = Cursor::new(bytes.as_ref()); + let commitment_from_cursor = Commitment::from_cursor(&mut cursor).unwrap(); + assert_eq!(commitment, commitment_from_cursor); + } + + #[cfg(feature = "host")] + #[test] + fn test_nullifier_to_bytes_roundtrip() { + let nullifier = Nullifier((0..32).collect::>().try_into().unwrap()); + let bytes = nullifier.to_byte_array(); + let mut cursor = Cursor::new(bytes.as_ref()); + let nullifier_from_cursor = Nullifier::from_cursor(&mut cursor).unwrap(); + assert_eq!(nullifier, nullifier_from_cursor); + } + + #[cfg(feature = "host")] + #[test] + fn test_account_to_bytes_roundtrip() { + let account = Account { + program_owner: [1, 2, 3, 4, 5, 6, 7, 8], + balance: 123456789012345678901234567890123456, + nonce: 42, + data: b"hola mundo".to_vec(), + }; + let bytes = account.to_bytes(); + let mut cursor = Cursor::new(bytes.as_ref()); + let account_from_cursor = Account::from_cursor(&mut cursor).unwrap(); + assert_eq!(account, account_from_cursor); + } } diff --git a/nssa/core/src/account/nullifier.rs b/nssa/core/src/account/nullifier.rs index 13f81db0..06c3e22f 100644 --- a/nssa/core/src/account/nullifier.rs +++ b/nssa/core/src/account/nullifier.rs @@ -23,7 +23,7 @@ impl From<&NullifierSecretKey> for NullifierPublicKey { pub type NullifierSecretKey = [u8; 32]; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] -pub struct Nullifier([u8; 32]); +pub struct Nullifier(pub(super) [u8; 32]); impl Nullifier { pub fn new(commitment: &Commitment, nsk: &NullifierSecretKey) -> Self { @@ -53,12 +53,12 @@ mod tests { #[test] fn test_from_secret_key() { let nsk = [ - 50, 139, 109, 225, 82, 86, 80, 108, 140, 248, 232, 229, 96, 80, 148, 250, 15, 9, 155, - 44, 196, 224, 115, 180, 160, 44, 113, 133, 15, 196, 253, 42, + 57, 5, 64, 115, 153, 56, 184, 51, 207, 238, 99, 165, 147, 214, 213, 151, 30, 251, 30, + 196, 134, 22, 224, 211, 237, 120, 136, 225, 188, 220, 249, 28, ]; let expected_Npk = NullifierPublicKey([ - 38, 90, 215, 216, 195, 66, 157, 77, 161, 59, 121, 18, 118, 37, 57, 199, 189, 251, 95, - 130, 12, 9, 171, 169, 140, 221, 87, 242, 46, 243, 111, 85, + 202, 120, 42, 189, 194, 218, 78, 244, 31, 6, 108, 169, 29, 61, 22, 221, 69, 138, 197, + 161, 241, 39, 142, 242, 242, 50, 188, 201, 99, 28, 176, 238, ]); let Npk = NullifierPublicKey::from(&nsk); assert_eq!(Npk, expected_Npk); diff --git a/nssa/core/src/error.rs b/nssa/core/src/error.rs new file mode 100644 index 00000000..b9e50208 --- /dev/null +++ b/nssa/core/src/error.rs @@ -0,0 +1,12 @@ +use std::io; + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum NssaCoreError { + #[error("Invalid transaction: {0}")] + DeserializationError(String), + + #[error("IO error: {0}")] + Io(#[from] io::Error), +} diff --git a/nssa/core/src/lib.rs b/nssa/core/src/lib.rs index 54556e53..c0f8d589 100644 --- a/nssa/core/src/lib.rs +++ b/nssa/core/src/lib.rs @@ -1,5 +1,7 @@ use serde::{Deserialize, Serialize}; +#[cfg(feature = "host")] +use crate::error::NssaCoreError; use crate::{ account::{ Account, AccountWithMetadata, Commitment, Nonce, Nullifier, NullifierPublicKey, @@ -8,9 +10,15 @@ use crate::{ program::{ProgramId, ProgramOutput}, }; +#[cfg(feature = "host")] +use std::io::Cursor; + pub mod account; pub mod program; +#[cfg(feature = "host")] +pub mod error; + pub type CommitmentSetDigest = [u32; 8]; pub type MembershipProof = Vec<[u8; 32]>; pub fn verify_membership_proof( @@ -51,6 +59,18 @@ impl EncryptedAccountData { // TODO: implement Self } + + #[cfg(feature = "host")] + pub fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result { + todo!() + } +} + +impl EncryptedAccountData { + pub fn to_bytes(&self) -> Vec { + // TODO: implement + vec![0] + } } #[derive(Serialize, Deserialize)] diff --git a/nssa/src/error.rs b/nssa/src/error.rs index de4a7ccb..038d2171 100644 --- a/nssa/src/error.rs +++ b/nssa/src/error.rs @@ -31,4 +31,9 @@ pub enum NssaError { #[error("Risc0 error: {0}")] ProgramProveFailed(String), + #[error("Invalid transaction: {0}")] + TransactionDeserializationError(String), + + #[error("Core error")] + Core(#[from] nssa_core::error::NssaCoreError), } diff --git a/nssa/src/privacy_preserving_transaction/encoding.rs b/nssa/src/privacy_preserving_transaction/encoding.rs new file mode 100644 index 00000000..2a19ebe6 --- /dev/null +++ b/nssa/src/privacy_preserving_transaction/encoding.rs @@ -0,0 +1,140 @@ +use std::io::{Cursor, Read}; + +use nssa_core::{ + EncryptedAccountData, + account::{Account, Commitment, Nullifier}, +}; + +use crate::{Address, error::NssaError}; + +use super::message::Message; + +const MESSAGE_ENCODING_PREFIX_LEN: usize = 37; +const MESSAGE_ENCODING_PREFIX: &[u8; MESSAGE_ENCODING_PREFIX_LEN] = + b"NSSA/v0.1/TxMessage/PrivacyPreserving"; + +impl Message { + pub(crate) fn to_bytes(&self) -> Vec { + let mut bytes = MESSAGE_ENCODING_PREFIX.to_vec(); + + // Public addresses + let public_addresses_len: u32 = self.public_addresses.len() as u32; + bytes.extend_from_slice(&public_addresses_len.to_le_bytes()); + for address in &self.public_addresses { + bytes.extend_from_slice(address.value()); + } + // Nonces + let nonces_len = self.nonces.len() as u32; + bytes.extend(&nonces_len.to_le_bytes()); + for nonce in &self.nonces { + bytes.extend(&nonce.to_le_bytes()); + } + // Public post states + let public_post_states_len: u32 = self.public_post_states.len() as u32; + bytes.extend_from_slice(&public_post_states_len.to_le_bytes()); + for account in &self.public_post_states { + bytes.extend_from_slice(&account.to_bytes()); + } + + // Encrypted post states + let encrypted_accounts_post_states_len: u32 = + self.encrypted_private_post_states.len() as u32; + for encrypted_account in &self.encrypted_private_post_states { + bytes.extend_from_slice(&encrypted_account.to_bytes()); + } + + // New commitments + let new_commitments_len: u32 = self.new_commitments.len() as u32; + for commitment in &self.new_commitments { + bytes.extend_from_slice(&commitment.to_byte_array()); + } + + // New nullifiers + let new_nullifiers_len: u32 = self.new_nullifiers.len() as u32; + for nullifier in &self.new_nullifiers { + bytes.extend_from_slice(&nullifier.to_byte_array()); + } + bytes + } + + pub(crate) fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result { + let prefix = { + let mut this = [0u8; MESSAGE_ENCODING_PREFIX_LEN]; + cursor.read_exact(&mut this)?; + this + }; + let prefix = { + let mut this = [0u8; MESSAGE_ENCODING_PREFIX_LEN]; + cursor.read_exact(&mut this)?; + this + }; + if &prefix != MESSAGE_ENCODING_PREFIX { + return Err(NssaError::TransactionDeserializationError( + "Invalid privacy preserving message prefix".to_string(), + )); + } + + let mut len_bytes = [0u8; 4]; + + // Public addresses + cursor.read_exact(&mut len_bytes)?; + let public_addresses_len = u32::from_le_bytes(len_bytes) as usize; + let mut public_addresses = Vec::with_capacity(public_addresses_len); + for _ in 0..public_addresses_len { + let mut value = [0u8; 32]; + cursor.read_exact(&mut value)?; + public_addresses.push(Address::new(value)) + } + + // Nonces + cursor.read_exact(&mut len_bytes)?; + let nonces_len = u32::from_le_bytes(len_bytes) as usize; + let mut nonces = Vec::with_capacity(nonces_len as usize); + for _ in 0..nonces_len { + let mut buf = [0u8; 16]; + cursor.read_exact(&mut buf)?; + nonces.push(u128::from_le_bytes(buf)) + } + + // Public post states + cursor.read_exact(&mut len_bytes)?; + let public_post_states_len = u32::from_le_bytes(len_bytes) as usize; + let mut public_post_states = Vec::with_capacity(public_post_states_len); + for _ in 0..public_post_states_len { + public_post_states.push(Account::from_cursor(cursor)?); + } + + // Encrypted private post states + cursor.read_exact(&mut len_bytes)?; + let encrypted_len = u32::from_le_bytes(len_bytes) as usize; + let mut encrypted_private_post_states = Vec::with_capacity(encrypted_len); + for _ in 0..encrypted_len { + encrypted_private_post_states.push(EncryptedAccountData::from_cursor(cursor)?); + } + + // New commitments + cursor.read_exact(&mut len_bytes)?; + let new_commitments_len = u32::from_le_bytes(len_bytes) as usize; + let mut new_commitments = Vec::with_capacity(new_commitments_len); + for _ in 0..new_commitments_len { + new_commitments.push(Commitment::from_cursor(cursor)?); + } + + // New nullifiers + cursor.read_exact(&mut len_bytes)?; + let new_nullifiers_len = u32::from_le_bytes(len_bytes) as usize; + let mut new_nullifiers = Vec::with_capacity(new_nullifiers_len); + for _ in 0..new_nullifiers_len { + new_nullifiers.push(Nullifier::from_cursor(cursor)?); + } + + Ok(Self { + public_addresses, + nonces, + public_post_states, + encrypted_private_post_states, + new_commitments, + new_nullifiers, + }) + } +} diff --git a/nssa/src/privacy_preserving_transaction/message.rs b/nssa/src/privacy_preserving_transaction/message.rs index b2461c15..f6e5e653 100644 --- a/nssa/src/privacy_preserving_transaction/message.rs +++ b/nssa/src/privacy_preserving_transaction/message.rs @@ -36,7 +36,7 @@ impl Message { } #[cfg(test)] -mod tests { +pub mod tests { use nssa_core::account::{ Account, Commitment, Nullifier, NullifierPublicKey, NullifierSecretKey, }; diff --git a/nssa/src/privacy_preserving_transaction/mod.rs b/nssa/src/privacy_preserving_transaction/mod.rs index 2d3bba38..8d77b90d 100644 --- a/nssa/src/privacy_preserving_transaction/mod.rs +++ b/nssa/src/privacy_preserving_transaction/mod.rs @@ -1,5 +1,6 @@ mod transaction; mod message; mod witness_set; +mod encoding; pub use transaction::PrivacyPreservingTransaction; diff --git a/nssa/src/privacy_preserving_transaction/transaction.rs b/nssa/src/privacy_preserving_transaction/transaction.rs index 055cc028..1b975917 100644 --- a/nssa/src/privacy_preserving_transaction/transaction.rs +++ b/nssa/src/privacy_preserving_transaction/transaction.rs @@ -146,3 +146,8 @@ fn n_unique(data: &[T]) -> usize { let set: HashSet<&T> = data.iter().collect(); set.len() } + +#[cfg(test)] +mod tests { + +} diff --git a/nssa/src/privacy_preserving_transaction/witness_set.rs b/nssa/src/privacy_preserving_transaction/witness_set.rs index 14056016..285a4754 100644 --- a/nssa/src/privacy_preserving_transaction/witness_set.rs +++ b/nssa/src/privacy_preserving_transaction/witness_set.rs @@ -9,7 +9,7 @@ pub struct WitnessSet { } impl WitnessSet { - pub fn for_message(message: &Message, private_keys: &[&PrivateKey]) -> Self { + pub fn for_message(message: &Message, proof: Proof, private_keys: &[&PrivateKey]) -> Self { todo!() } diff --git a/nssa/src/public_transaction/encoding.rs b/nssa/src/public_transaction/encoding.rs index 3c004a08..0e3c8ba4 100644 --- a/nssa/src/public_transaction/encoding.rs +++ b/nssa/src/public_transaction/encoding.rs @@ -8,8 +8,9 @@ use crate::{ public_transaction::{Message, WitnessSet}, }; -const MESSAGE_ENCODING_PREFIX_LEN: usize = 19; -const MESSAGE_ENCODING_PREFIX: &[u8; MESSAGE_ENCODING_PREFIX_LEN] = b"NSSA/v0.1/TxMessage"; +const MESSAGE_ENCODING_PREFIX_LEN: usize = 37; +const MESSAGE_ENCODING_PREFIX: &[u8; MESSAGE_ENCODING_PREFIX_LEN] = + b"NSSA/v0.1/TxMessage/Public\0\0\0\0\0\0\0\0\0\0\0"; impl Message { /// Serializes a `Message` into bytes in the following layout: @@ -52,7 +53,12 @@ impl Message { cursor.read_exact(&mut this)?; this }; - assert_eq!(&prefix, MESSAGE_ENCODING_PREFIX); + if &prefix != MESSAGE_ENCODING_PREFIX { + return Err(NssaError::TransactionDeserializationError( + "Invalid public message prefix".to_string(), + )); + } + let program_id: ProgramId = { let mut this = [0u32; 8]; for item in &mut this { diff --git a/nssa/src/state.rs b/nssa/src/state.rs index aa42f3dc..101fa25b 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -21,6 +21,10 @@ impl CommitmentSet { // TODO: implement [0; 8] } + + fn contains(&self, commitment: &Commitment) -> bool { + self.0.contains(commitment) + } } type NullifierSet = HashSet; @@ -139,14 +143,28 @@ impl V01State { &self, new_commitments: &[Commitment], ) -> Result<(), NssaError> { - todo!() + for commitment in new_commitments.iter() { + if self.private_state.0.contains(commitment) { + return Err(NssaError::InvalidInput( + "Commitment already seen".to_string(), + )); + } + } + Ok(()) } pub(crate) fn check_nullifiers_are_new( &self, new_nullifiers: &[Nullifier], ) -> Result<(), NssaError> { - todo!() + for nullifier in new_nullifiers.iter() { + if self.private_state.1.contains(nullifier) { + return Err(NssaError::InvalidInput( + "Nullifier already seen".to_string(), + )); + } + } + Ok(()) } }