diff --git a/Cargo.toml b/Cargo.toml index 9d31f3e..2b98867 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,7 @@ version = "0.8.5" [workspace.dependencies.k256] features = ["ecdsa-core", "arithmetic", "expose-field", "serde", "pem"] -version = "0.13.4" +version = "0.13.3" [workspace.dependencies.elliptic-curve] features = ["arithmetic"] diff --git a/ci_scripts/test-ubuntu.sh b/ci_scripts/test-ubuntu.sh index 0d22626..6a1710b 100644 --- a/ci_scripts/test-ubuntu.sh +++ b/ci_scripts/test-ubuntu.sh @@ -4,8 +4,8 @@ curl -L https://risczero.com/install | bash /home/runner/.risc0/bin/rzup install source env.sh -cargo test --release +RISC0_DEV_MODE=1 cargo test --release cd integration_tests export NSSA_WALLET_HOME_DIR=$(pwd)/configs/debug/wallet/ export RUST_LOG=info -cargo run $(pwd)/configs/debug all \ No newline at end of file +cargo run $(pwd)/configs/debug all diff --git a/nssa/Cargo.toml b/nssa/Cargo.toml index 4ebe658..98a0adf 100644 --- a/nssa/Cargo.toml +++ b/nssa/Cargo.toml @@ -6,14 +6,16 @@ edition = "2024" [dependencies] thiserror = "2.0.12" risc0-zkvm = "3.0.3" -nssa-core = { path = "core" } +nssa-core = { path = "core", features = ["host"] } program-methods = { path = "program_methods" } serde = "1.0.219" sha2 = "0.10.9" secp256k1 = "0.31.1" rand = "0.8" +borsh = "1.5.7" +bytemuck = "1.13" hex = "0.4.3" -anyhow.workspace = true [dev-dependencies] test-program-methods = { path = "test_program_methods" } +hex-literal = "1.0.0" diff --git a/nssa/core/Cargo.toml b/nssa/core/Cargo.toml index aba5e45..2eb0ce2 100644 --- a/nssa/core/Cargo.toml +++ b/nssa/core/Cargo.toml @@ -4,5 +4,13 @@ version = "0.1.0" edition = "2024" [dependencies] -risc0-zkvm = "3.0.3" +risc0-zkvm = { version = "3.0.3" } serde = { version = "1.0", default-features = false } +thiserror = { version = "2.0.12", optional = true } +bytemuck = { version = "1.13", optional = true } +chacha20 = { version = "0.9", default-features = false } +k256 = { version = "0.13.3", optional = true } + +[features] +default = [] +host = ["thiserror", "bytemuck", "k256"] diff --git a/nssa/core/src/account/mod.rs b/nssa/core/src/account.rs similarity index 82% rename from nssa/core/src/account/mod.rs rename to nssa/core/src/account.rs index 9d564c4..688611e 100644 --- a/nssa/core/src/account/mod.rs +++ b/nssa/core/src/account.rs @@ -1,12 +1,12 @@ -use serde::{Deserialize, Serialize}; - use crate::program::ProgramId; +use serde::{Deserialize, Serialize}; pub type Nonce = u128; type Data = Vec; /// Account to be used both in public and private contexts -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq)] +#[cfg_attr(any(feature = "host", test), derive(Debug))] pub struct Account { pub program_owner: ProgramId, pub balance: u128, @@ -14,7 +14,8 @@ pub struct Account { pub nonce: Nonce, } -#[derive(Clone, Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] +#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] pub struct AccountWithMetadata { pub account: Account, pub is_authorized: bool, diff --git a/nssa/core/src/circuit_io.rs b/nssa/core/src/circuit_io.rs new file mode 100644 index 0000000..e619b2d --- /dev/null +++ b/nssa/core/src/circuit_io.rs @@ -0,0 +1,94 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + Commitment, CommitmentSetDigest, MembershipProof, Nullifier, NullifierPublicKey, + NullifierSecretKey, SharedSecretKey, + account::{Account, AccountWithMetadata, Nonce}, + encryption::Ciphertext, + program::{ProgramId, ProgramOutput}, +}; + +#[derive(Serialize, Deserialize)] +pub struct PrivacyPreservingCircuitInput { + pub program_output: ProgramOutput, + pub visibility_mask: Vec, + pub private_account_nonces: Vec, + pub private_account_keys: Vec<(NullifierPublicKey, SharedSecretKey)>, + pub private_account_auth: Vec<(NullifierSecretKey, MembershipProof)>, + pub program_id: ProgramId, +} + +#[derive(Serialize, Deserialize)] +#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] +pub struct PrivacyPreservingCircuitOutput { + pub public_pre_states: Vec, + pub public_post_states: Vec, + pub ciphertexts: Vec, + pub new_commitments: Vec, + pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>, +} + +#[cfg(feature = "host")] +impl PrivacyPreservingCircuitOutput { + pub fn to_bytes(&self) -> Vec { + bytemuck::cast_slice(&risc0_zkvm::serde::to_vec(&self).unwrap()).to_vec() + } +} + +#[cfg(feature = "host")] +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + Commitment, Nullifier, NullifierPublicKey, + account::{Account, AccountWithMetadata}, + }; + use risc0_zkvm::serde::from_slice; + + #[test] + fn test_privacy_preserving_circuit_output_to_bytes_is_compatible_with_from_slice() { + let output = PrivacyPreservingCircuitOutput { + public_pre_states: vec![ + AccountWithMetadata { + account: Account { + program_owner: [1, 2, 3, 4, 5, 6, 7, 8], + balance: 12345678901234567890, + data: b"test data".to_vec(), + nonce: 18446744073709551614, + }, + is_authorized: true, + }, + AccountWithMetadata { + account: Account { + program_owner: [9, 9, 9, 8, 8, 8, 7, 7], + balance: 123123123456456567112, + data: b"test data".to_vec(), + nonce: 9999999999999999999999, + }, + is_authorized: false, + }, + ], + public_post_states: vec![Account { + program_owner: [1, 2, 3, 4, 5, 6, 7, 8], + balance: 100, + data: b"post state data".to_vec(), + nonce: 18446744073709551615, + }], + ciphertexts: vec![Ciphertext(vec![255, 255, 1, 1, 2, 2])], + new_commitments: vec![Commitment::new( + &NullifierPublicKey::from(&[1; 32]), + &Account::default(), + )], + new_nullifiers: vec![( + Nullifier::new( + &Commitment::new(&NullifierPublicKey::from(&[2; 32]), &Account::default()), + &[1; 32], + ), + [0xab; 32], + )], + }; + let bytes = output.to_bytes(); + let output_from_slice: PrivacyPreservingCircuitOutput = from_slice(&bytes).unwrap(); + assert_eq!(output, output_from_slice); + } +} diff --git a/nssa/core/src/commitment.rs b/nssa/core/src/commitment.rs new file mode 100644 index 0000000..bc22c8f --- /dev/null +++ b/nssa/core/src/commitment.rs @@ -0,0 +1,63 @@ +use risc0_zkvm::sha::{Impl, Sha256}; +use serde::{Deserialize, Serialize}; + +use crate::{NullifierPublicKey, account::Account}; + +#[derive(Serialize, Deserialize)] +#[cfg_attr(any(feature = "host", test), derive(Debug, Clone, PartialEq, Eq, Hash))] +pub struct Commitment(pub(super) [u8; 32]); + +impl Commitment { + pub fn new(npk: &NullifierPublicKey, account: &Account) -> Self { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&npk.to_byte_array()); + 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()) + } +} + +pub type CommitmentSetDigest = [u8; 32]; + +pub type MembershipProof = (usize, Vec<[u8; 32]>); + +pub fn compute_digest_for_path( + commitment: &Commitment, + proof: &MembershipProof, +) -> CommitmentSetDigest { + let value_bytes = commitment.to_byte_array(); + let mut result: [u8; 32] = Impl::hash_bytes(&value_bytes) + .as_bytes() + .try_into() + .unwrap(); + let mut level_index = proof.0; + for node in &proof.1 { + let is_left_child = level_index & 1 == 0; + if is_left_child { + let mut bytes = [0u8; 64]; + bytes[..32].copy_from_slice(&result); + bytes[32..].copy_from_slice(node); + result = Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap(); + } else { + let mut bytes = [0u8; 64]; + bytes[..32].copy_from_slice(node); + bytes[32..].copy_from_slice(&result); + result = Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap(); + } + level_index >>= 1; + } + result +} diff --git a/nssa/core/src/encoding.rs b/nssa/core/src/encoding.rs new file mode 100644 index 0000000..dd586de --- /dev/null +++ b/nssa/core/src/encoding.rs @@ -0,0 +1,218 @@ +// TODO: Consider switching to deriving Borsh +#[cfg(feature = "host")] +use std::io::Cursor; + +#[cfg(feature = "host")] +use std::io::Read; + +use crate::account::Account; + +#[cfg(feature = "host")] +use crate::encryption::shared_key_derivation::Secp256k1Point; + +use crate::encryption::Ciphertext; + +#[cfg(feature = "host")] +use crate::error::NssaCoreError; + +use crate::Commitment; +#[cfg(feature = "host")] +use crate::Nullifier; +use crate::NullifierPublicKey; + +impl Account { + pub fn to_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + for word in &self.program_owner { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes.extend_from_slice(&self.balance.to_le_bytes()); + bytes.extend_from_slice(&self.nonce.to_le_bytes()); + 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 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 + } +} + +#[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)) + } +} + +impl Ciphertext { + pub fn to_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + let ciphertext_length: u32 = self.0.len() as u32; + bytes.extend_from_slice(&ciphertext_length.to_le_bytes()); + bytes.extend_from_slice(&self.0); + + bytes + } + + #[cfg(feature = "host")] + pub fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result { + let mut u32_bytes = [0; 4]; + + cursor.read_exact(&mut u32_bytes)?; + let ciphertext_lenght = u32::from_le_bytes(u32_bytes); + let mut ciphertext = vec![0; ciphertext_lenght as usize]; + cursor.read_exact(&mut ciphertext)?; + + Ok(Self(ciphertext)) + } +} + +#[cfg(feature = "host")] +impl Secp256k1Point { + pub fn to_bytes(&self) -> [u8; 33] { + self.0.clone().try_into().unwrap() + } + + pub fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result { + let mut value = vec![0; 33]; + cursor.read_exact(&mut value)?; + Ok(Self(value)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_enconding() { + let account = Account { + program_owner: [1, 2, 3, 4, 5, 6, 7, 8], + balance: 123456789012345678901234567890123456, + nonce: 42, + data: b"hola mundo".to_vec(), + }; + + // program owner || balance || nonce || data_len || data + 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, 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); + } + + #[cfg(feature = "host")] + #[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/encryption/mod.rs b/nssa/core/src/encryption/mod.rs new file mode 100644 index 0000000..b79e75c --- /dev/null +++ b/nssa/core/src/encryption/mod.rs @@ -0,0 +1,77 @@ +use chacha20::{ + ChaCha20, + cipher::{KeyIvInit, StreamCipher}, +}; +use risc0_zkvm::sha::{Impl, Sha256}; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "host")] +pub(crate) mod shared_key_derivation; + +#[cfg(feature = "host")] +pub use shared_key_derivation::{EphemeralPublicKey, EphemeralSecretKey, IncomingViewingPublicKey}; + +use crate::{Commitment, account::Account}; + +#[derive(Serialize, Deserialize, Clone)] +pub struct SharedSecretKey([u8; 32]); + +pub struct EncryptionScheme; + +#[derive(Serialize, Deserialize)] +#[cfg_attr(any(feature = "host", test), derive(Debug, Clone, PartialEq, Eq))] +pub struct Ciphertext(pub(crate) Vec); + +impl EncryptionScheme { + pub fn encrypt( + account: &Account, + shared_secret: &SharedSecretKey, + commitment: &Commitment, + output_index: u32, + ) -> Ciphertext { + let mut buffer = account.to_bytes().to_vec(); + Self::symmetric_transform(&mut buffer, shared_secret, commitment, output_index); + Ciphertext(buffer) + } + + fn symmetric_transform( + buffer: &mut [u8], + shared_secret: &SharedSecretKey, + commitment: &Commitment, + output_index: u32, + ) { + let key = Self::kdf(shared_secret, commitment, output_index); + let mut cipher = ChaCha20::new(&key.into(), &[0; 12].into()); + cipher.apply_keystream(buffer); + } + + fn kdf( + shared_secret: &SharedSecretKey, + commitment: &Commitment, + output_index: u32, + ) -> [u8; 32] { + let mut bytes = Vec::new(); + + bytes.extend_from_slice(b"NSSA/v0.1/KDF-SHA256"); + bytes.extend_from_slice(&shared_secret.0); + bytes.extend_from_slice(&commitment.to_byte_array()); + bytes.extend_from_slice(&output_index.to_le_bytes()); + + Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap() + } + + #[cfg(feature = "host")] + pub fn decrypt( + ciphertext: &Ciphertext, + shared_secret: &SharedSecretKey, + commitment: &Commitment, + output_index: u32, + ) -> Option { + use std::io::Cursor; + let mut buffer = ciphertext.0.to_owned(); + Self::symmetric_transform(&mut buffer, shared_secret, commitment, output_index); + + let mut cursor = Cursor::new(buffer.as_slice()); + Account::from_cursor(&mut cursor).ok() + } +} diff --git a/nssa/core/src/encryption/shared_key_derivation.rs b/nssa/core/src/encryption/shared_key_derivation.rs new file mode 100644 index 0000000..c735105 --- /dev/null +++ b/nssa/core/src/encryption/shared_key_derivation.rs @@ -0,0 +1,56 @@ +use serde::{Deserialize, Serialize}; + +use k256::{ + AffinePoint, EncodedPoint, FieldBytes, ProjectivePoint, Scalar, + elliptic_curve::{ + PrimeField, + sec1::{FromEncodedPoint, ToEncodedPoint}, + }, +}; + +use crate::SharedSecretKey; + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub struct Secp256k1Point(pub(crate) Vec); + +impl Secp256k1Point { + pub fn from_scalar(value: [u8; 32]) -> Secp256k1Point { + let x_bytes: FieldBytes = value.into(); + let x = Scalar::from_repr(x_bytes).unwrap(); + + let p = ProjectivePoint::GENERATOR * x; + let q = AffinePoint::from(p); + let enc = q.to_encoded_point(true); + + Self(enc.as_bytes().to_vec()) + } +} + +pub type EphemeralSecretKey = [u8; 32]; +pub type EphemeralPublicKey = Secp256k1Point; +pub type IncomingViewingPublicKey = Secp256k1Point; +impl From<&EphemeralSecretKey> for EphemeralPublicKey { + fn from(value: &EphemeralSecretKey) -> Self { + Secp256k1Point::from_scalar(*value) + } +} + +impl SharedSecretKey { + pub fn new(scalar: &[u8; 32], point: &Secp256k1Point) -> Self { + let scalar = Scalar::from_repr((*scalar).into()).unwrap(); + let point: [u8; 33] = point.0.clone().try_into().unwrap(); + + let encoded = EncodedPoint::from_bytes(point).unwrap(); + let pubkey_affine = AffinePoint::from_encoded_point(&encoded).unwrap(); + + let shared = ProjectivePoint::from(pubkey_affine) * scalar; + let shared_affine = shared.to_affine(); + + let encoded = shared_affine.to_encoded_point(false); + let x_bytes_slice = encoded.x().unwrap(); + let mut x_bytes = [0u8; 32]; + x_bytes.copy_from_slice(x_bytes_slice); + + Self(x_bytes) + } +} diff --git a/nssa/core/src/error.rs b/nssa/core/src/error.rs new file mode 100644 index 0000000..8f05323 --- /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("Deserialization error: {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 d20620e..2275e31 100644 --- a/nssa/core/src/lib.rs +++ b/nssa/core/src/lib.rs @@ -1,2 +1,15 @@ pub mod account; +mod circuit_io; +mod commitment; +mod encoding; +pub mod encryption; +mod nullifier; pub mod program; + +pub use circuit_io::{PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput}; +pub use commitment::{Commitment, CommitmentSetDigest, MembershipProof, compute_digest_for_path}; +pub use encryption::{EncryptionScheme, SharedSecretKey}; +pub use nullifier::{Nullifier, NullifierPublicKey, NullifierSecretKey}; + +#[cfg(feature = "host")] +pub mod error; diff --git a/nssa/core/src/nullifier.rs b/nssa/core/src/nullifier.rs new file mode 100644 index 0000000..d1410de --- /dev/null +++ b/nssa/core/src/nullifier.rs @@ -0,0 +1,68 @@ +use risc0_zkvm::sha::{Impl, Sha256}; +use serde::{Deserialize, Serialize}; + +use crate::Commitment; + +#[derive(Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(any(feature = "host", test), derive(Debug, Clone, Hash))] +pub struct NullifierPublicKey(pub(super) [u8; 32]); + +impl From<&NullifierSecretKey> for NullifierPublicKey { + fn from(value: &NullifierSecretKey) -> Self { + let mut bytes = Vec::new(); + const PREFIX: &[u8; 9] = b"NSSA_keys"; + const SUFFIX_1: &[u8; 1] = &[7]; + const SUFFIX_2: &[u8; 22] = &[0; 22]; + bytes.extend_from_slice(PREFIX); + bytes.extend_from_slice(value); + bytes.extend_from_slice(SUFFIX_1); + bytes.extend_from_slice(SUFFIX_2); + Self(Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap()) + } +} + +pub type NullifierSecretKey = [u8; 32]; + +#[derive(Serialize, Deserialize)] +#[cfg_attr(any(feature = "host", test), derive(Debug, Clone, PartialEq, Eq, Hash))] +pub struct Nullifier(pub(super) [u8; 32]); + +impl Nullifier { + pub fn new(commitment: &Commitment, nsk: &NullifierSecretKey) -> Self { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&commitment.to_byte_array()); + bytes.extend_from_slice(nsk); + Self(Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_constructor() { + let commitment = Commitment((0..32u8).collect::>().try_into().unwrap()); + let nsk = [0x42; 32]; + let expected_nullifier = Nullifier([ + 97, 87, 111, 191, 0, 44, 125, 145, 237, 104, 31, 230, 203, 254, 68, 176, 126, 17, 240, + 205, 249, 143, 11, 43, 15, 198, 189, 219, 191, 49, 36, 61, + ]); + let nullifier = Nullifier::new(&commitment, &nsk); + assert_eq!(nullifier, expected_nullifier); + } + + #[test] + fn test_from_secret_key() { + let nsk = [ + 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([ + 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/program.rs b/nssa/core/src/program.rs index 9b99a61..d284bbc 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -1,17 +1,42 @@ use crate::account::{Account, AccountWithMetadata}; use risc0_zkvm::serde::Deserializer; use risc0_zkvm::{DeserializeOwned, guest::env}; +use serde::{Deserialize, Serialize}; pub type ProgramId = [u32; 8]; pub type InstructionData = Vec; pub const DEFAULT_PROGRAM_ID: ProgramId = [0; 8]; -pub fn read_nssa_inputs() -> (Vec, T) { +pub struct ProgramInput { + pub pre_states: Vec, + pub instruction: T, +} + +#[derive(Serialize, Deserialize, Clone)] +#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] +pub struct ProgramOutput { + pub pre_states: Vec, + pub post_states: Vec, +} + +pub fn read_nssa_inputs() -> ProgramInput { let pre_states: Vec = env::read(); let words: InstructionData = env::read(); - let instruction_data = T::deserialize(&mut Deserializer::new(words.as_ref())).unwrap(); - (pre_states, instruction_data) + let instruction = T::deserialize(&mut Deserializer::new(words.as_ref())).unwrap(); + ProgramInput { + pre_states, + instruction, + } } + +pub fn write_nssa_outputs(pre_states: Vec, post_states: Vec) { + let output = ProgramOutput { + pre_states, + post_states, + }; + env::commit(&output); +} + /// Validates well-behaved program execution /// /// # Parameters diff --git a/nssa/program_methods/guest/Cargo.lock b/nssa/program_methods/guest/Cargo.lock index dea19c8..18285e9 100644 --- a/nssa/program_methods/guest/Cargo.lock +++ b/nssa/program_methods/guest/Cargo.lock @@ -505,6 +505,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "chrono" version = "0.4.41" @@ -518,6 +529,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "cobs" version = "0.3.0" @@ -1319,6 +1340,15 @@ dependencies = [ "serde", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "io-uring" version = "0.7.9" @@ -1541,6 +1571,7 @@ checksum = "a5b0c77c1b780822bc749a33e39aeb2c07584ab93332303babeabb645298a76e" name = "nssa-core" version = "0.1.0" dependencies = [ + "chacha20", "risc0-zkvm", "serde", ] diff --git a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs index a89b090..9e7f399 100644 --- a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs +++ b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs @@ -1,17 +1,17 @@ -use nssa_core::program::read_nssa_inputs; -use risc0_zkvm::guest::env; - -type Instruction = u128; +use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput}; /// A transfer of balance program. /// To be used both in public and private contexts. fn main() { // Read input accounts. // It is expected to receive only two accounts: [sender_account, receiver_account] - let (input_accounts, balance_to_move) = read_nssa_inputs::(); + let ProgramInput { + pre_states, + instruction: balance_to_move, + } = read_nssa_inputs(); // Continue only if input_accounts is an array of two elements - let [sender, receiver] = match input_accounts.try_into() { + let [sender, receiver] = match pre_states.try_into() { Ok(array) => array, Err(_) => return, }; @@ -32,5 +32,5 @@ fn main() { sender_post.balance -= balance_to_move; receiver_post.balance += balance_to_move; - env::commit(&vec![sender_post, receiver_post]); + write_nssa_outputs(vec![sender, receiver], vec![sender_post, receiver_post]); } diff --git a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs new file mode 100644 index 0000000..83f593a --- /dev/null +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -0,0 +1,155 @@ +use risc0_zkvm::{guest::env, serde::to_vec}; + +use nssa_core::{ + account::{Account, AccountWithMetadata}, + compute_digest_for_path, + encryption::Ciphertext, + program::{validate_execution, ProgramOutput, DEFAULT_PROGRAM_ID}, + Commitment, CommitmentSetDigest, EncryptionScheme, Nullifier, NullifierPublicKey, + PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, +}; + +fn main() { + let PrivacyPreservingCircuitInput { + program_output, + visibility_mask, + private_account_nonces, + private_account_keys, + private_account_auth, + program_id, + } = env::read(); + + // TODO: Check that `program_execution_proof` is one of the allowed built-in programs + // assert_eq!(program_id, AUTHENTICATED_TRANSFER_PROGRAM_ID); + + // Check that `program_output` is consistent with the execution of the corresponding program. + env::verify(program_id, &to_vec(&program_output).unwrap()).unwrap(); + + let ProgramOutput { + pre_states, + post_states, + } = program_output; + + // Check that the program is well behaved. + // See the # Programs section for the definition of the `validate_execution` method. + validate_execution(&pre_states, &post_states, program_id); + + let n_accounts = pre_states.len(); + if visibility_mask.len() != n_accounts { + panic!(); + } + + // These lists will be the public outputs of this circuit + // and will be populated next. + let mut public_pre_states: Vec = Vec::new(); + let mut public_post_states: Vec = Vec::new(); + let mut ciphertexts: Vec = Vec::new(); + let mut new_commitments: Vec = Vec::new(); + let mut new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)> = Vec::new(); + + let mut private_nonces_iter = private_account_nonces.iter(); + let mut private_keys_iter = private_account_keys.iter(); + let mut private_auth_iter = private_account_auth.iter(); + + let mut output_index = 0; + for i in 0..n_accounts { + match visibility_mask[i] { + 0 => { + // Public account + public_pre_states.push(pre_states[i].clone()); + + let mut post = post_states[i].clone(); + post.nonce += 1; + if post.program_owner == DEFAULT_PROGRAM_ID { + // Claim account + post.program_owner = program_id; + } + public_post_states.push(post); + } + 1 | 2 => { + let new_nonce = private_nonces_iter.next().expect("Missing private nonce"); + let (npk, shared_secret) = private_keys_iter.next().expect("Missing keys"); + + if visibility_mask[i] == 1 { + // Private account with authentication + let (nsk, membership_proof) = + private_auth_iter.next().expect("Missing private auth"); + + // Verify the nullifier public key + let expected_npk = NullifierPublicKey::from(nsk); + if &expected_npk != npk { + panic!("Nullifier public key mismatch"); + } + + // Compute commitment set digest associated with provided auth path + let commitment_pre = Commitment::new(npk, &pre_states[i].account); + let set_digest = compute_digest_for_path(&commitment_pre, membership_proof); + + // Check pre_state authorization + if !pre_states[i].is_authorized { + panic!("Pre-state not authorized"); + } + + // Compute nullifier + let nullifier = Nullifier::new(&commitment_pre, nsk); + new_nullifiers.push((nullifier, set_digest)); + } else { + if pre_states[i].account != Account::default() { + panic!("Found new private account with non default values."); + } + + if pre_states[i].is_authorized { + panic!("Found new private account marked as authorized."); + } + } + + // Update post-state with new nonce + let mut post_with_updated_values = post_states[i].clone(); + post_with_updated_values.nonce = *new_nonce; + + if post_with_updated_values.program_owner == DEFAULT_PROGRAM_ID { + // Claim account + post_with_updated_values.program_owner = program_id; + } + + // Compute commitment + let commitment_post = Commitment::new(npk, &post_with_updated_values); + + // Encrypt and push post state + let encrypted_account = EncryptionScheme::encrypt( + &post_with_updated_values, + shared_secret, + &commitment_post, + output_index, + ); + + new_commitments.push(commitment_post); + ciphertexts.push(encrypted_account); + output_index += 1; + } + _ => panic!("Invalid visibility mask value"), + } + } + + if private_nonces_iter.next().is_some() { + panic!("Too many nonces."); + } + + if private_keys_iter.next().is_some() { + panic!("Too many private accounts keys."); + } + + if private_auth_iter.next().is_some() { + panic!("Too many private account authentication keys."); + } + + let output = PrivacyPreservingCircuitOutput { + public_pre_states, + public_post_states, + ciphertexts, + new_commitments, + new_nullifiers, + }; + + env::commit(&output); +} diff --git a/nssa/src/error.rs b/nssa/src/error.rs index 11a2f41..1d6d6ad 100644 --- a/nssa/src/error.rs +++ b/nssa/src/error.rs @@ -7,9 +7,6 @@ pub enum NssaError { #[error("Invalid input: {0}")] InvalidInput(String), - #[error("Risc0 error: {0}")] - ProgramExecutionFailed(String), - #[error("Program violated execution rules")] InvalidProgramBehavior, @@ -24,4 +21,28 @@ pub enum NssaError { #[error("Invalid Public Key")] InvalidPublicKey, + + #[error("Risc0 error: {0}")] + ProgramWriteInputFailed(String), + + #[error("Risc0 error: {0}")] + ProgramExecutionFailed(String), + + #[error("Risc0 error: {0}")] + ProgramProveFailed(String), + + #[error("Invalid transaction: {0}")] + TransactionDeserializationError(String), + + #[error("Core error")] + Core(#[from] nssa_core::error::NssaCoreError), + + #[error("Program output deserialization error: {0}")] + ProgramOutputDeserializationError(String), + + #[error("Circuit output deserialization error: {0}")] + CircuitOutputDeserializationError(String), + + #[error("Invalid privacy preserving execution circuit proof")] + InvalidPrivacyPreservingProof, } diff --git a/nssa/src/lib.rs b/nssa/src/lib.rs index 9555b14..ed88047 100644 --- a/nssa/src/lib.rs +++ b/nssa/src/lib.rs @@ -1,11 +1,16 @@ pub mod address; pub mod error; +mod merkle_tree; +mod privacy_preserving_transaction; pub mod program; pub mod public_transaction; mod signature; mod state; pub use address::Address; +pub use privacy_preserving_transaction::{ + PrivacyPreservingTransaction, circuit::execute_and_prove, +}; pub use public_transaction::PublicTransaction; pub use signature::PrivateKey; pub use signature::PublicKey; diff --git a/nssa/src/merkle_tree/default_values.rs b/nssa/src/merkle_tree/default_values.rs new file mode 100644 index 0000000..0316644 --- /dev/null +++ b/nssa/src/merkle_tree/default_values.rs @@ -0,0 +1,130 @@ +pub(crate) const DEFAULT_VALUES: [[u8; 32]; 32] = [ + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, + ], + [ + 245, 165, 253, 66, 209, 106, 32, 48, 39, 152, 239, 110, 211, 9, 151, 155, 67, 0, 61, 35, + 32, 217, 240, 232, 234, 152, 49, 169, 39, 89, 251, 75, + ], + [ + 219, 86, 17, 78, 0, 253, 212, 193, 248, 92, 137, 43, 243, 90, 201, 168, 146, 137, 170, 236, + 177, 235, 208, 169, 108, 222, 96, 106, 116, 139, 93, 113, + ], + [ + 199, 128, 9, 253, 240, 127, 197, 106, 17, 241, 34, 55, 6, 88, 163, 83, 170, 165, 66, 237, + 99, 228, 76, 75, 193, 95, 244, 205, 16, 90, 179, 60, + ], + [ + 83, 109, 152, 131, 127, 45, 209, 101, 165, 93, 94, 234, 233, 20, 133, 149, 68, 114, 213, + 111, 36, 109, 242, 86, 191, 60, 174, 25, 53, 42, 18, 60, + ], + [ + 158, 253, 224, 82, 170, 21, 66, 159, 174, 5, 186, 212, 208, 177, 215, 198, 77, 166, 77, 3, + 215, 161, 133, 74, 88, 140, 44, 184, 67, 12, 13, 48, + ], + [ + 216, 141, 223, 238, 212, 0, 168, 117, 85, 150, 178, 25, 66, 193, 73, 126, 17, 76, 48, 46, + 97, 24, 41, 15, 145, 230, 119, 41, 118, 4, 31, 161, + ], + [ + 135, 235, 13, 219, 165, 126, 53, 246, 210, 134, 103, 56, 2, 164, 175, 89, 117, 226, 37, 6, + 199, 207, 76, 100, 187, 107, 229, 238, 17, 82, 127, 44, + ], + [ + 38, 132, 100, 118, 253, 95, 197, 74, 93, 67, 56, 81, 103, 201, 81, 68, 242, 100, 63, 83, + 60, 200, 91, 185, 209, 107, 120, 47, 141, 125, 177, 147, + ], + [ + 80, 109, 134, 88, 45, 37, 36, 5, 184, 64, 1, 135, 146, 202, 210, 191, 18, 89, 241, 239, 90, + 165, 248, 135, 225, 60, 178, 240, 9, 79, 81, 225, + ], + [ + 255, 255, 10, 215, 230, 89, 119, 47, 149, 52, 193, 149, 200, 21, 239, 196, 1, 78, 241, 225, + 218, 237, 68, 4, 192, 99, 133, 209, 17, 146, 233, 43, + ], + [ + 108, 240, 65, 39, 219, 5, 68, 28, 216, 51, 16, 122, 82, 190, 133, 40, 104, 137, 14, 67, 23, + 230, 160, 42, 180, 118, 131, 170, 117, 150, 66, 32, + ], + [ + 183, 208, 95, 135, 95, 20, 0, 39, 239, 81, 24, 162, 36, 123, 187, 132, 206, 143, 47, 15, + 17, 35, 98, 48, 133, 218, 247, 150, 12, 50, 159, 95, + ], + [ + 223, 106, 245, 245, 187, 219, 107, 233, 239, 138, 166, 24, 228, 191, 128, 115, 150, 8, 103, + 23, 30, 41, 103, 111, 139, 40, 77, 234, 106, 8, 168, 94, + ], + [ + 181, 141, 144, 15, 94, 24, 46, 60, 80, 239, 116, 150, 158, 161, 108, 119, 38, 197, 73, 117, + 124, 194, 53, 35, 195, 105, 88, 125, 167, 41, 55, 132, + ], + [ + 212, 154, 117, 2, 255, 207, 176, 52, 11, 29, 120, 133, 104, 133, 0, 202, 48, 129, 97, 167, + 249, 107, 98, 223, 157, 8, 59, 113, 252, 200, 242, 187, + ], + [ + 143, 230, 177, 104, 146, 86, 192, 211, 133, 244, 47, 91, 190, 32, 39, 162, 44, 25, 150, + 225, 16, 186, 151, 193, 113, 211, 229, 148, 141, 233, 43, 235, + ], + [ + 141, 13, 99, 195, 158, 186, 222, 133, 9, 224, 174, 60, 156, 56, 118, 251, 95, 161, 18, 190, + 24, 249, 5, 236, 172, 254, 203, 146, 5, 118, 3, 171, + ], + [ + 149, 238, 200, 178, 229, 65, 202, 212, 233, 29, 227, 131, 133, 242, 224, 70, 97, 159, 84, + 73, 108, 35, 130, 203, 108, 172, 213, 185, 140, 38, 245, 164, + ], + [ + 248, 147, 233, 8, 145, 119, 117, 182, 43, 255, 35, 41, 77, 187, 227, 161, 205, 142, 108, + 193, 195, 91, 72, 1, 136, 123, 100, 106, 111, 129, 241, 127, + ], + [ + 205, 219, 167, 181, 146, 227, 19, 51, 147, 193, 97, 148, 250, 199, 67, 26, 191, 47, 84, + 133, 237, 113, 29, 178, 130, 24, 60, 129, 158, 8, 235, 170, + ], + [ + 138, 141, 127, 227, 175, 140, 170, 8, 90, 118, 57, 168, 50, 0, 20, 87, 223, 185, 18, 138, + 128, 97, 20, 42, 208, 51, 86, 41, 255, 35, 255, 156, + ], + [ + 254, 179, 195, 55, 215, 165, 26, 111, 191, 0, 185, 227, 76, 82, 225, 201, 25, 92, 150, 155, + 212, 231, 160, 191, 213, 29, 92, 91, 237, 156, 17, 103, + ], + [ + 231, 31, 10, 168, 60, 195, 46, 223, 190, 250, 159, 77, 62, 1, 116, 202, 133, 24, 46, 236, + 159, 58, 9, 246, 166, 192, 223, 99, 119, 165, 16, 215, + ], + [ + 49, 32, 111, 168, 10, 80, 187, 106, 190, 41, 8, 80, 88, 241, 98, 18, 33, 42, 96, 238, 200, + 240, 73, 254, 203, 146, 216, 200, 224, 168, 75, 192, + ], + [ + 33, 53, 43, 254, 203, 237, 221, 233, 147, 131, 159, 97, 76, 61, 172, 10, 62, 227, 117, 67, + 249, 180, 18, 177, 97, 153, 220, 21, 142, 35, 181, 68, + ], + [ + 97, 158, 49, 39, 36, 187, 109, 124, 49, 83, 237, 157, 231, 145, 215, 100, 163, 102, 179, + 137, 175, 19, 197, 139, 248, 168, 217, 4, 129, 164, 103, 101, + ], + [ + 124, 221, 41, 134, 38, 130, 80, 98, 141, 12, 16, 227, 133, 197, 140, 97, 145, 230, 251, + 224, 81, 145, 188, 192, 79, 19, 63, 44, 234, 114, 193, 196, + ], + [ + 132, 137, 48, 189, 123, 168, 202, 197, 70, 97, 7, 33, 19, 251, 39, 136, 105, 224, 123, 184, + 88, 127, 145, 57, 41, 51, 55, 77, 1, 123, 203, 225, + ], + [ + 136, 105, 255, 44, 34, 178, 140, 193, 5, 16, 217, 133, 50, 146, 128, 51, 40, 190, 79, 176, + 232, 4, 149, 232, 187, 141, 39, 31, 91, 136, 150, 54, + ], + [ + 181, 254, 40, 231, 159, 27, 133, 15, 134, 88, 36, 108, 233, 182, 161, 231, 180, 159, 192, + 109, 183, 20, 62, 143, 224, 180, 242, 176, 197, 82, 58, 92, + ], + [ + 152, 94, 146, 159, 112, 175, 40, 208, 189, 209, 169, 10, 128, 143, 151, 127, 89, 124, 124, + 119, 140, 72, 158, 152, 211, 189, 137, 16, 211, 26, 192, 247, + ], +]; diff --git a/nssa/src/merkle_tree/mod.rs b/nssa/src/merkle_tree/mod.rs new file mode 100644 index 0000000..2306efd --- /dev/null +++ b/nssa/src/merkle_tree/mod.rs @@ -0,0 +1,547 @@ +use sha2::{Digest, Sha256}; + +mod default_values; + +type Value = [u8; 32]; +type Node = [u8; 32]; + +/// Compute parent as the hash of two child nodes +fn hash_two(left: &Node, right: &Node) -> Node { + let mut hasher = Sha256::new(); + hasher.update(left); + hasher.update(right); + hasher.finalize().into() +} + +fn hash_value(value: &Value) -> Node { + let mut hasher = Sha256::new(); + hasher.update(value); + hasher.finalize().into() +} + +#[cfg_attr(test, derive(Debug, PartialEq, Eq))] +pub struct MerkleTree { + nodes: Vec, + capacity: usize, + length: usize, +} + +impl MerkleTree { + pub fn root(&self) -> Node { + let root_index = self.root_index(); + *self.get_node(root_index) + } + + fn root_index(&self) -> usize { + let tree_depth = self.depth(); + let capacity_depth = self.capacity.trailing_zeros() as usize; + + if tree_depth == capacity_depth { + 0 + } else { + (1 << (capacity_depth - tree_depth)) - 1 + } + } + + /// Number of levels required to hold all values + fn depth(&self) -> usize { + self.length.next_power_of_two().trailing_zeros() as usize + } + + fn get_node(&self, index: usize) -> &Node { + &self.nodes[index] + } + + fn set_node(&mut self, index: usize, node: Node) { + self.nodes[index] = node; + } + + pub fn with_capacity(capacity: usize) -> Self { + let capacity = capacity.next_power_of_two(); + let total_depth = capacity.trailing_zeros() as usize; + + let nodes = default_values::DEFAULT_VALUES[..(total_depth + 1)] + .iter() + .rev() + .enumerate() + .flat_map(|(level, default_value)| std::iter::repeat_n(default_value, 1 << level)) + .cloned() + .collect(); + + Self { + nodes, + capacity, + length: 0, + } + } + + fn reallocate_to_double_capacity(&mut self) { + let old_capacity = self.capacity; + let new_capacity = old_capacity << 1; + + let mut this = Self::with_capacity(new_capacity); + + for (index, value) in self.nodes.iter().enumerate() { + let offset = prev_power_of_two(index + 1); + let new_index = index + offset; + this.set_node(new_index, *value); + } + + this.length = self.length; + + *self = this; + } + + pub fn insert(&mut self, value: Value) -> usize { + if self.length == self.capacity { + self.reallocate_to_double_capacity(); + } + + let new_index = self.length; + + let mut node_index = new_index + self.capacity - 1; + let mut node_hash = hash_value(&value); + + self.set_node(node_index, node_hash); + self.length += 1; + + for _ in 0..self.depth() { + let parent_index = (node_index - 1) >> 1; + let left_child = self.get_node((parent_index << 1) + 1); + let right_child = self.get_node((parent_index << 1) + 2); + node_hash = hash_two(left_child, right_child); + self.set_node(parent_index, node_hash); + node_index = parent_index; + } + + new_index + } + + pub fn get_authentication_path_for(&self, index: usize) -> Option> { + if index >= self.length { + return None; + } + + let mut path = Vec::with_capacity(self.depth()); + + let mut node_index = self.capacity + index - 1; + let root_index = self.root_index(); + + while node_index != root_index { + let parent_index = (node_index - 1) >> 1; + let is_left_child = node_index & 1 == 1; + let sibling_index = if is_left_child { + node_index + 1 + } else { + node_index - 1 + }; + path.push(*self.get_node(sibling_index)); + node_index = parent_index; + } + + Some(path) + } +} + +fn prev_power_of_two(x: usize) -> usize { + if x == 0 { + return 0; + } + 1 << (usize::BITS as usize - x.leading_zeros() as usize - 1) +} + +#[cfg(test)] +mod tests { + impl MerkleTree { + pub fn new(values: &[Value]) -> Self { + let mut this = Self::with_capacity(values.len()); + for value in values.iter().cloned() { + this.insert(value); + } + this + } + } + + use hex_literal::hex; + + use super::*; + #[test] + fn test_empty_merkle_tree() { + let tree = MerkleTree::with_capacity(4); + let expected_root = + hex!("0000000000000000000000000000000000000000000000000000000000000000"); + assert_eq!(tree.root(), expected_root); + assert_eq!(tree.capacity, 4); + assert_eq!(tree.length, 0); + } + + #[test] + fn test_merkle_tree_0() { + let values = [[0; 32]]; + let tree = MerkleTree::new(&values); + assert_eq!(tree.root(), hash_value(&[0; 32])); + assert_eq!(tree.capacity, 1); + assert_eq!(tree.length, 1); + } + + #[test] + fn test_merkle_tree_1() { + let values = [[1; 32], [2; 32], [3; 32], [4; 32]]; + let tree = MerkleTree::new(&values); + let expected_root = + hex!("48c73f7821a58a8d2a703e5b39c571c0aa20cf14abcd0af8f2b955bc202998de"); + assert_eq!(tree.root(), expected_root); + assert_eq!(tree.capacity, 4); + assert_eq!(tree.length, 4) + } + + #[test] + fn test_merkle_tree_2() { + let values = [[1; 32], [2; 32], [3; 32], [0; 32]]; + let tree = MerkleTree::new(&values); + let expected_root = + hex!("c9bbb83096df85157a146e7d770455a98412dee0633187ee86fee6c8a45b831a"); + assert_eq!(tree.root(), expected_root); + assert_eq!(tree.capacity, 4); + assert_eq!(tree.length, 4); + } + + #[test] + fn test_merkle_tree_3() { + let values = [[1; 32], [2; 32], [3; 32]]; + let tree = MerkleTree::new(&values); + let expected_root = + hex!("c8d3d8d2b13f27ceeccdc699119871f9f32ea7ed86ff45d0ad11f77b28cd7568"); + assert_eq!(tree.root(), expected_root); + assert_eq!(tree.capacity, 4); + assert_eq!(tree.length, 3); + } + + #[test] + fn test_merkle_tree_4() { + let values = [[11; 32], [12; 32], [13; 32], [14; 32], [15; 32]]; + let tree = MerkleTree::new(&values); + let expected_root = + hex!("ef418aed5aa20702d4d94c92da79a4012f2e36f1008bfdb3cd1e38749dca2499"); + + assert_eq!(tree.root(), expected_root); + assert_eq!(tree.capacity, 8); + assert_eq!(tree.length, 5); + } + + #[test] + fn test_merkle_tree_5() { + let values = [ + [11; 32], [12; 32], [12; 32], [13; 32], [14; 32], [15; 32], [15; 32], [13; 32], + [13; 32], [15; 32], [11; 32], + ]; + let tree = MerkleTree::new(&values); + let expected_root = + hex!("3f72d2ff55921a86c48e5988ec3e19ee9d0d5aa3e23197842970a903508ed767"); + assert_eq!(tree.root(), expected_root); + assert_eq!(tree.capacity, 16); + assert_eq!(tree.length, 11); + } + + #[test] + fn test_merkle_tree_6() { + let values = [[1; 32], [2; 32], [3; 32], [4; 32], [5; 32]]; + let tree = MerkleTree::new(&values); + let expected_root = + hex!("069cb8259a06fe6edb3fa7ff7933a6dd7dca6fca299314379794a688926c3792"); + assert_eq!(tree.root(), expected_root); + } + + #[test] + fn test_with_capacity_4() { + let tree = MerkleTree::with_capacity(4); + + assert_eq!(tree.length, 0); + assert_eq!(tree.nodes.len(), 7); + for i in 3..7 { + assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[0], "{i}"); + } + for i in 1..3 { + assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[1], "{i}"); + } + assert_eq!(*tree.get_node(0), default_values::DEFAULT_VALUES[2]); + } + + #[test] + fn test_with_capacity_5() { + let tree = MerkleTree::with_capacity(5); + + assert_eq!(tree.length, 0); + assert_eq!(tree.nodes.len(), 15); + for i in 7..15 { + assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[0]) + } + for i in 3..7 { + assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[1]) + } + for i in 1..3 { + assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[2]) + } + assert_eq!(*tree.get_node(0), default_values::DEFAULT_VALUES[3]) + } + + #[test] + fn test_with_capacity_6() { + let mut tree = MerkleTree::with_capacity(100); + + let values = [[1; 32], [2; 32], [3; 32], [4; 32]]; + + let expected_root = + hex!("48c73f7821a58a8d2a703e5b39c571c0aa20cf14abcd0af8f2b955bc202998de"); + + assert_eq!(0, tree.insert(values[0])); + assert_eq!(1, tree.insert(values[1])); + assert_eq!(2, tree.insert(values[2])); + assert_eq!(3, tree.insert(values[3])); + + assert_eq!(tree.root(), expected_root); + } + + #[test] + fn test_with_capacity_7() { + let mut tree = MerkleTree::with_capacity(599); + + let values = [[1; 32], [2; 32], [3; 32]]; + + let expected_root = + hex!("c8d3d8d2b13f27ceeccdc699119871f9f32ea7ed86ff45d0ad11f77b28cd7568"); + + assert_eq!(0, tree.insert(values[0])); + assert_eq!(1, tree.insert(values[1])); + assert_eq!(2, tree.insert(values[2])); + + assert_eq!(tree.root(), expected_root); + } + + #[test] + fn test_with_capacity_8() { + let mut tree = MerkleTree::with_capacity(1); + + let values = [[1; 32], [2; 32], [3; 32]]; + + let expected_root = + hex!("c8d3d8d2b13f27ceeccdc699119871f9f32ea7ed86ff45d0ad11f77b28cd7568"); + + assert_eq!(0, tree.insert(values[0])); + assert_eq!(1, tree.insert(values[1])); + assert_eq!(2, tree.insert(values[2])); + + assert_eq!(tree.root(), expected_root); + } + + #[test] + fn test_insert_value_1() { + let mut tree = MerkleTree::with_capacity(1); + + let values = [[1; 32], [2; 32], [3; 32]]; + let expected_tree = MerkleTree::new(&values); + + assert_eq!(0, tree.insert(values[0])); + assert_eq!(1, tree.insert(values[1])); + assert_eq!(2, tree.insert(values[2])); + + assert_eq!(expected_tree, tree); + } + + #[test] + fn test_insert_value_2() { + let mut tree = MerkleTree::with_capacity(1); + + let values = [[1; 32], [2; 32], [3; 32], [4; 32]]; + let expected_tree = MerkleTree::new(&values); + + assert_eq!(0, tree.insert(values[0])); + assert_eq!(1, tree.insert(values[1])); + assert_eq!(2, tree.insert(values[2])); + assert_eq!(3, tree.insert(values[3])); + + assert_eq!(expected_tree, tree); + } + + #[test] + fn test_insert_value_3() { + let mut tree = MerkleTree::with_capacity(1); + + let values = [[11; 32], [12; 32], [13; 32], [14; 32], [15; 32]]; + let expected_tree = MerkleTree::new(&values); + + tree.insert(values[0]); + tree.insert(values[1]); + tree.insert(values[2]); + tree.insert(values[3]); + tree.insert(values[4]); + + assert_eq!(expected_tree, tree); + } + + // Reference implementation + fn verify_authentication_path(value: &Value, index: usize, path: &[Node], root: &Node) -> bool { + let mut result = hash_value(value); + let mut level_index = index; + for node in path { + let is_left_child = level_index & 1 == 0; + if is_left_child { + result = hash_two(&result, node); + } else { + result = hash_two(node, &result); + } + level_index >>= 1; + } + &result == root + } + + #[test] + fn test_authentication_path_1() { + let values = [[1; 32], [2; 32], [3; 32], [4; 32]]; + let tree = MerkleTree::new(&values); + let expected_authentication_path = vec![ + hex!("9f4fb68f3e1dac82202f9aa581ce0bbf1f765df0e9ac3c8c57e20f685abab8ed"), + hex!("50a27d4746f357cb700cbe9d4883b77fb64f0128828a3489dc6a6f21ddbf2414"), + ]; + + let authentication_path = tree.get_authentication_path_for(2).unwrap(); + assert_eq!(authentication_path, expected_authentication_path); + } + + #[test] + fn test_authentication_path_2() { + let values = [[1; 32], [2; 32], [3; 32]]; + let tree = MerkleTree::new(&values); + let expected_authentication_path = vec![ + hex!("75877bb41d393b5fb8455ce60ecd8dda001d06316496b14dfa7f895656eeca4a"), + hex!("a41b855d2db4de9052cd7be5ec67d6586629cb9f6e3246a4afa5ba313f07a9c5"), + ]; + + let authentication_path = tree.get_authentication_path_for(0).unwrap(); + assert_eq!(authentication_path, expected_authentication_path); + } + + #[test] + fn test_authentication_path_3() { + let values = [[1; 32], [2; 32], [3; 32], [4; 32], [5; 32]]; + let tree = MerkleTree::new(&values); + let expected_authentication_path = vec![ + hex!("0000000000000000000000000000000000000000000000000000000000000000"), + hex!("f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b"), + hex!("48c73f7821a58a8d2a703e5b39c571c0aa20cf14abcd0af8f2b955bc202998de"), + ]; + + let authentication_path = tree.get_authentication_path_for(4).unwrap(); + assert_eq!(authentication_path, expected_authentication_path); + } + + #[test] + fn test_authentication_path_4() { + let values = [[1; 32], [2; 32], [3; 32], [4; 32], [5; 32]]; + let tree = MerkleTree::new(&values); + assert!(tree.get_authentication_path_for(5).is_none()); + } + + #[test] + fn test_authentication_path_5() { + let values = [[1; 32], [2; 32], [3; 32], [4; 32], [5; 32]]; + let tree = MerkleTree::new(&values); + let index = 4; + let value = values[index]; + let path = tree.get_authentication_path_for(index).unwrap(); + assert!(verify_authentication_path( + &value, + index, + &path, + &tree.root() + )); + } + + #[test] + fn test_tree_with_63_insertions() { + let values = [ + hex!("cd00acab0f45736e6c6311f1953becc0b69a062e7c2a7310875d28bdf9ef9c5b"), + hex!("0df5a6afbcc7bf126caf7084acfc593593ab512e6ca433c61c1a922be40a04ea"), + hex!("23c1258620266c7bedb6d1ee32f6da9413e4010ace975239dccb34e727e07c40"), + hex!("f33ccc3a11476b0ef62326ca5ec292056759b05e6a28023d2d1ce66165611353"), + hex!("77f914ab016b8049f6bea7704000e413a393865918a3824f9285c3db0aacff23"), + hex!("910a1c23188e54d57fd167ddb0f8bf68c6b70ed9ec76ef56c4b7f2632f82ca7f"), + hex!("047ee85526197d1e7403a559cf6d2f22c1926c8ad59481a2e2f1b697af45e40b"), + hex!("9d355cf89fb382ae34bf80566b28489278d10f2cebb5b0ea42fab1bac5adae0c"), + hex!("604018b95232596b2685a9bc737b6cccb53b10e483d2d9a2f4a755410b02a188"), + hex!("a16708ef7b6bf1796063addaf57d6a566b6f87b0bbe42af43a4590d05f1684cb"), + hex!("820f2dfa271cd2fd41e1452406d5dad552c85c1223c45d45dbd7446759fdc6b8"), + hex!("680b6912d7e219f8805d4d28adb4428dd78fea0dc1b8cdb2412645c4b1962c88"), + hex!("14d5471ce6c45506753982b17cac5790ac7bc29e6f388f31052d7dfd62b294e5"), + hex!("8b364200172b777d4aa16d2098b5eb98ac3dd4a1b9597e5c2bf6f6930031f230"), + hex!("9bb45b910711874339dda8a21a9aad73822286f5e52d7d3de0ed78dfbba329a5"), + hex!("d6806d5df5cb25ce5d531042f09b3cb34fb9e47c61182b63cccd9d44392f6027"), + hex!("b8cfa90ebc8fd09c04682d93a08fddd3e8e57715174dcc92451edd191264a58b"), + hex!("3463c7f81d00f809b3dfa83195447c927fb4045b3913dac6f45bee6c4010d7ed"), + hex!("1d6ad7f7d677905feb506c58f4b404a79370ebc567296abea3a368b61d5a8239"), + hex!("a58085ecf00963cb22da23c901b9b3ddc56462bb96ff03c923d67708e10dd29c"), + hex!("c3319f4a65fb5bbb8447137b0972c03cbd84ebf7d9da194e0fcbd68c2d4d5bdb"), + hex!("4aa31e90e0090faf3648d05e5d5499df2c78ebed4d6e6c23d8147de5d67dae73"), + hex!("9f33b1d2c8bc7bd265336de1033ede6344bc41260313bdcb43f1108b83b9be92"), + hex!("6500d4ad93d41c16ec81eaa5e70f173194aabe5c1072ac263b5727296f5b7cac"), + hex!("3584f5d260003669fad98786e13171376b0f19410cb232ce65606cbff79e6768"), + hex!("c8410946ebf56f13141c894a34ced85a5230088af70dcea581e44f52847830ac"), + hex!("71dd90281cdebb70422f2d04ae446d5d2d5ea64b803c16128d37e3fcd5d1a4cc"), + hex!("c05acf8d77ab4d659a538bd35af590864a7ad9c055ff5d6cda9d5aecfccecba3"), + hex!("f1df98822ea084cce9021aa9cc81b1746cd1e84a75690da63e10fd877633ed77"), + hex!("2ca822bc8f67bceb0a71a0d06fea7349036ef3e5ec21795a851e4182bd35ce01"), + hex!("7fd2179abc3bcf89b4d8092988ba8c23952b3bbd3d7caea6b5ea0c13cf19f68b"), + hex!("91b6ad516e017f6aa5a2e95776538bd3a3e933c1b1d32bb5e0f00a9db63c9c24"), + hex!("cd31a8b5eef5ca0be5ef1cb261d0bf0a74d774a3152bb99739cfd296a1d0b85e"), + hex!("3fb16f48b2bf93f3815979e6638f975d7f935088ec37db0be0f07965fbc78339"), + hex!("c60c61b99bf486af5f4bf780a69860dafcd35c1474306a8575666fb5449bcec0"), + hex!("8048d0d7e14091251f3f6c6b10bf6b5880a014b513f9f8c2395501dbffa6192a"), + hex!("778b5af10b9dbe80b60a8e4f0bb91caf4476bcb812801099760754ae623fbd84"), + hex!("d3ac25467920a4e08998b7a3226b8b54bfe66ac58cfedc71f15b2402fee0054a"), + hex!("029aa94598fae2961a0d43937b8a9a3138bcfeae99a7cb15f77fac7c506f8432"), + hex!("2eee5ef52fe669cb6882a68c893abdc1262dcf4424e4ba7a479da7cf1c10171d"), + hex!("de3fb3d070e3a90f0eed8b5e65088a8dc0e4e3c342b9c0bf33bab714eae5dfec"), + hex!("14d40177e833ab45bbfdc5f2b11fba7efaebb3f69facc554f24b549a2efe8538"), + hex!("5734355069702448774fb2df95f1d562e1b9fe1514aeb6b922554ee9d2d01068"), + hex!("8a273d49ac110343cec2cf3359d16eb2906b446bd9ec9833e2a640cebc8d5155"), + hex!("e3fa984dd3cbeb9a7e827ed32d3d4e6a6ba643a55d82be97d9ddb06ee809fa3e"), + hex!("90b1d5a364e17c8b7965396b06ec6e13749b5fc16500731518ad8fc30ae33e77"), + hex!("7517376541b2e8ec83cbab04522b54a26610908a9872feb663451385aea58eb1"), + hex!("5cba2e4cf7448e526d161133c4b2ea7c919ac4813a7308612595f46f11dea6cd"), + hex!("c721911b300bec0691c8a2dfaabfef1d66b7b6258918914d3c3ad690729f05b7"), + hex!("d0d0a70d8ae0d27806fa0b711c507290c260a89cbca0436d339d1dccdd087d62"), + hex!("2a625c28ea763c5e82dd0a93ecfca7ec371ccbb363cd42be359c2c875f58009d"), + hex!("174ef0119932ed890397d9f3837dd85f9100558b6fc9085d4af947ae8cf74bbc"), + hex!("b497bc267151e8efa3c6daa461e6804b01a3f05f44f1f4d5b41d5f0d3f5219b1"), + hex!("e987e91f5734630ddd7e6b58733b4fcdbc316ee9e8cac0e94c36c91cf58e59cc"), + hex!("55019ad8bbe656c51eb042190c1c8da53f42baf43fd2350ebea38fc7cca2fae3"), + hex!("c45a638edd18a6d9f5ad20b870c81b8626459bcb22dae7d58add7a6b6c6a84a8"), + hex!("d42d3a5fb2ad50b2027fe5a36d59dd71e49a63e4b1b299073c96bbf7ba5d68a1"), + hex!("9599e561054bcd3f647eb018ab0b069d3176497d42be9c4466551cbb959be47c"), + hex!("42f33b23775327ff71aea6569548255f3cc9929da73373cc9bb1743d417f7cda"), + hex!("ab24294f44fc6fdbeb96e0f6e93c4f6d97d035b73b9a337c353e18c6d0603bdd"), + hex!("33954ec63520334f99b640a2982ac966b68c363fed383d621a1ab573934f1d33"), + hex!("5e2a1f7df963d1fd8f50a285387cfbb5df581426619b325563e20bf7886c62b7"), + hex!("13ffde471d4e27c473254e766fd1328ad80c42cab4d4955cffeae43d866f86e5"), + ]; + + let expected_root = + hex!("1cf9b214217d7823f9de51b8f6cb34d0a99436a3a1bb762f90b815672a6afcc0"); + + let mut tree_less_capacity = MerkleTree::with_capacity(1); + let mut tree_exact_capacity = MerkleTree::with_capacity(64); + let mut tree_more_capacity = MerkleTree::with_capacity(128); + + for value in &values { + tree_less_capacity.insert(*value); + tree_exact_capacity.insert(*value); + tree_more_capacity.insert(*value); + } + + assert_eq!(tree_more_capacity.root(), expected_root); + assert_eq!(tree_less_capacity.root(), expected_root); + assert_eq!(tree_exact_capacity.root(), expected_root); + } +} + +// diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs new file mode 100644 index 0000000..ed32f98 --- /dev/null +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -0,0 +1,271 @@ +use nssa_core::{ + MembershipProof, NullifierPublicKey, NullifierSecretKey, PrivacyPreservingCircuitInput, + PrivacyPreservingCircuitOutput, SharedSecretKey, + account::AccountWithMetadata, + program::{InstructionData, ProgramOutput}, +}; +use risc0_zkvm::{ExecutorEnv, InnerReceipt, Receipt, default_prover}; + +use crate::{error::NssaError, program::Program}; + +use program_methods::{PRIVACY_PRESERVING_CIRCUIT_ELF, PRIVACY_PRESERVING_CIRCUIT_ID}; + +/// Proof of the privacy preserving execution circuit +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Proof(Vec); + +impl Proof { + pub(crate) fn is_valid_for(&self, circuit_output: &PrivacyPreservingCircuitOutput) -> bool { + let inner: InnerReceipt = borsh::from_slice(&self.0).unwrap(); + let receipt = Receipt::new(inner, circuit_output.to_bytes()); + receipt.verify(PRIVACY_PRESERVING_CIRCUIT_ID).is_ok() + } +} + +/// Generates a proof of the execution of a NSSA program inside the privacy preserving execution +/// circuit +pub fn execute_and_prove( + pre_states: &[AccountWithMetadata], + instruction_data: &InstructionData, + visibility_mask: &[u8], + private_account_nonces: &[u128], + private_account_keys: &[(NullifierPublicKey, SharedSecretKey)], + private_account_auth: &[(NullifierSecretKey, MembershipProof)], + program: &Program, +) -> Result<(PrivacyPreservingCircuitOutput, Proof), NssaError> { + let inner_receipt = execute_and_prove_program(program, pre_states, instruction_data)?; + + let program_output: ProgramOutput = inner_receipt + .journal + .decode() + .map_err(|e| NssaError::ProgramOutputDeserializationError(e.to_string()))?; + + let circuit_input = PrivacyPreservingCircuitInput { + program_output, + visibility_mask: visibility_mask.to_vec(), + private_account_nonces: private_account_nonces.to_vec(), + private_account_keys: private_account_keys.to_vec(), + private_account_auth: private_account_auth.to_vec(), + program_id: program.id(), + }; + + // Prove circuit. + let mut env_builder = ExecutorEnv::builder(); + env_builder.add_assumption(inner_receipt); + env_builder.write(&circuit_input).unwrap(); + let env = env_builder.build().unwrap(); + let prover = default_prover(); + let prove_info = prover.prove(env, PRIVACY_PRESERVING_CIRCUIT_ELF).unwrap(); + + let proof = Proof(borsh::to_vec(&prove_info.receipt.inner)?); + + let circuit_output: PrivacyPreservingCircuitOutput = prove_info + .receipt + .journal + .decode() + .map_err(|e| NssaError::CircuitOutputDeserializationError(e.to_string()))?; + + Ok((circuit_output, proof)) +} + +fn execute_and_prove_program( + program: &Program, + pre_states: &[AccountWithMetadata], + instruction_data: &InstructionData, +) -> Result { + // Write inputs to the program + let mut env_builder = ExecutorEnv::builder(); + Program::write_inputs(pre_states, instruction_data, &mut env_builder)?; + let env = env_builder.build().unwrap(); + + // Prove the program + let prover = default_prover(); + Ok(prover + .prove(env, program.elf()) + .map_err(|e| NssaError::ProgramProveFailed(e.to_string()))? + .receipt) +} + +#[cfg(test)] +mod tests { + use nssa_core::{ + Commitment, EncryptionScheme, Nullifier, + account::{Account, AccountWithMetadata}, + }; + + use crate::{ + privacy_preserving_transaction::circuit::execute_and_prove, + program::Program, + state::{ + CommitmentSet, + tests::{test_private_account_keys_1, test_private_account_keys_2}, + }, + }; + + use super::*; + + #[test] + fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts() { + let program = Program::authenticated_transfer_program(); + let sender = AccountWithMetadata { + account: Account { + balance: 100, + ..Account::default() + }, + is_authorized: true, + }; + + let recipient = AccountWithMetadata { + account: Account::default(), + is_authorized: false, + }; + + let balance_to_move: u128 = 37; + + let expected_sender_post = Account { + program_owner: program.id(), + balance: 100 - balance_to_move, + nonce: 1, + data: vec![], + }; + + let expected_recipient_post = Account { + program_owner: program.id(), + balance: balance_to_move, + nonce: 0xdeadbeef, + data: vec![], + }; + + let expected_sender_pre = sender.clone(); + let recipient_keys = test_private_account_keys_1(); + + let esk = [3; 32]; + let shared_secret = SharedSecretKey::new(&esk, &recipient_keys.ivk()); + + let (output, proof) = execute_and_prove( + &[sender, recipient], + &Program::serialize_instruction(balance_to_move).unwrap(), + &[0, 2], + &[0xdeadbeef], + &[(recipient_keys.npk(), shared_secret.clone())], + &[], + &Program::authenticated_transfer_program(), + ) + .unwrap(); + + assert!(proof.is_valid_for(&output)); + + let [sender_pre] = output.public_pre_states.try_into().unwrap(); + let [sender_post] = output.public_post_states.try_into().unwrap(); + assert_eq!(sender_pre, expected_sender_pre); + assert_eq!(sender_post, expected_sender_post); + assert_eq!(output.new_commitments.len(), 1); + assert_eq!(output.new_nullifiers.len(), 0); + assert_eq!(output.ciphertexts.len(), 1); + + let recipient_post = EncryptionScheme::decrypt( + &output.ciphertexts[0], + &shared_secret, + &output.new_commitments[0], + 0, + ) + .unwrap(); + assert_eq!(recipient_post, expected_recipient_post); + } + + #[test] + fn prove_privacy_preserving_execution_circuit_fully_private() { + let sender_pre = AccountWithMetadata { + account: Account { + balance: 100, + nonce: 0xdeadbeef, + ..Account::default() + }, + is_authorized: true, + }; + let sender_keys = test_private_account_keys_1(); + let recipient_keys = test_private_account_keys_2(); + let commitment_sender = Commitment::new(&sender_keys.npk(), &sender_pre.account); + + let recipient = AccountWithMetadata { + account: Account::default(), + is_authorized: false, + }; + let balance_to_move: u128 = 37; + + let mut commitment_set = CommitmentSet::with_capacity(2); + commitment_set.extend(std::slice::from_ref(&commitment_sender)); + + let expected_new_nullifiers = vec![( + Nullifier::new(&commitment_sender, &sender_keys.nsk), + commitment_set.digest(), + )]; + + let program = Program::authenticated_transfer_program(); + + let expected_private_account_1 = Account { + program_owner: program.id(), + balance: 100 - balance_to_move, + nonce: 0xdeadbeef1, + ..Default::default() + }; + let expected_private_account_2 = Account { + program_owner: program.id(), + balance: balance_to_move, + nonce: 0xdeadbeef2, + ..Default::default() + }; + let expected_new_commitments = vec![ + Commitment::new(&sender_keys.npk(), &expected_private_account_1), + Commitment::new(&recipient_keys.npk(), &expected_private_account_2), + ]; + + let esk_1 = [3; 32]; + let shared_secret_1 = SharedSecretKey::new(&esk_1, &sender_keys.ivk()); + + let esk_2 = [5; 32]; + let shared_secret_2 = SharedSecretKey::new(&esk_2, &recipient_keys.ivk()); + + let (output, proof) = execute_and_prove( + &[sender_pre.clone(), recipient], + &Program::serialize_instruction(balance_to_move).unwrap(), + &[1, 2], + &[0xdeadbeef1, 0xdeadbeef2], + &[ + (sender_keys.npk(), shared_secret_1.clone()), + (recipient_keys.npk(), shared_secret_2.clone()), + ], + &[( + sender_keys.nsk, + commitment_set.get_proof_for(&commitment_sender).unwrap(), + )], + &program, + ) + .unwrap(); + + assert!(proof.is_valid_for(&output)); + assert!(output.public_pre_states.is_empty()); + assert!(output.public_post_states.is_empty()); + assert_eq!(output.new_commitments, expected_new_commitments); + assert_eq!(output.new_nullifiers, expected_new_nullifiers); + assert_eq!(output.ciphertexts.len(), 2); + + let sender_post = EncryptionScheme::decrypt( + &output.ciphertexts[0], + &shared_secret_1, + &expected_new_commitments[0], + 0, + ) + .unwrap(); + assert_eq!(sender_post, expected_private_account_1); + + let recipient_post = EncryptionScheme::decrypt( + &output.ciphertexts[1], + &shared_secret_2, + &expected_new_commitments[1], + 1, + ) + .unwrap(); + assert_eq!(recipient_post, expected_private_account_2); + } +} diff --git a/nssa/src/privacy_preserving_transaction/encoding.rs b/nssa/src/privacy_preserving_transaction/encoding.rs new file mode 100644 index 0000000..b5d4950 --- /dev/null +++ b/nssa/src/privacy_preserving_transaction/encoding.rs @@ -0,0 +1,170 @@ +use std::io::{Cursor, Read}; + +use nssa_core::{ + Commitment, Nullifier, + account::Account, + encryption::{Ciphertext, EphemeralPublicKey}, +}; + +use crate::{ + Address, error::NssaError, privacy_preserving_transaction::message::EncryptedAccountData, +}; + +use super::message::Message; + +const MESSAGE_ENCODING_PREFIX_LEN: usize = 22; +const MESSAGE_ENCODING_PREFIX: &[u8; MESSAGE_ENCODING_PREFIX_LEN] = b"\x01/NSSA/v0.1/TxMessage/"; + +impl EncryptedAccountData { + pub(crate) fn to_bytes(&self) -> Vec { + let mut bytes = self.ciphertext.to_bytes(); + bytes.extend_from_slice(&self.epk.to_bytes()); + bytes.push(self.view_tag); + bytes + } + + pub fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result { + let ciphertext = Ciphertext::from_cursor(cursor)?; + let epk = EphemeralPublicKey::from_cursor(cursor)?; + + let mut tag_bytes = [0; 1]; + cursor.read_exact(&mut tag_bytes)?; + let view_tag = tag_bytes[0]; + + Ok(Self { + ciphertext, + epk, + view_tag, + }) + } +} + +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; + bytes.extend_from_slice(&encrypted_accounts_post_states_len.to_le_bytes()); + 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; + bytes.extend_from_slice(&new_commitments_len.to_le_bytes()); + 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; + bytes.extend_from_slice(&new_nullifiers_len.to_le_bytes()); + for (nullifier, commitment_set_digest) in &self.new_nullifiers { + bytes.extend_from_slice(&nullifier.to_byte_array()); + bytes.extend_from_slice(commitment_set_digest); + } + + bytes + } + + #[allow(unused)] + 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 + }; + 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); + 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 { + let nullifier = Nullifier::from_cursor(cursor)?; + let mut commitment_set_digest = [0; 32]; + cursor.read_exact(&mut commitment_set_digest)?; + new_nullifiers.push((nullifier, commitment_set_digest)); + } + + 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 new file mode 100644 index 0000000..4877e3d --- /dev/null +++ b/nssa/src/privacy_preserving_transaction/message.rs @@ -0,0 +1,111 @@ +use nssa_core::{ + Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput, + account::{Account, Nonce}, + encryption::{Ciphertext, EphemeralPublicKey}, +}; + +use crate::{Address, error::NssaError}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EncryptedAccountData { + pub(crate) ciphertext: Ciphertext, + pub(crate) epk: EphemeralPublicKey, + pub(crate) view_tag: u8, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Message { + pub(crate) public_addresses: Vec
, + pub(crate) nonces: Vec, + pub(crate) public_post_states: Vec, + pub(crate) encrypted_private_post_states: Vec, + pub(crate) new_commitments: Vec, + pub(crate) new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>, +} + +impl Message { + pub fn try_from_circuit_output( + public_addresses: Vec
, + nonces: Vec, + ephemeral_public_keys: Vec, + output: PrivacyPreservingCircuitOutput, + ) -> Result { + if ephemeral_public_keys.len() != output.ciphertexts.len() { + return Err(NssaError::InvalidInput( + "Ephemeral public keys and ciphertexts length mismatch".into(), + )); + } + + let encrypted_private_post_states = output + .ciphertexts + .into_iter() + .zip(ephemeral_public_keys) + .map(|(ciphertext, epk)| EncryptedAccountData { + ciphertext, + epk, + view_tag: 0, // TODO: implement + }) + .collect(); + Ok(Self { + public_addresses, + nonces, + public_post_states: output.public_post_states, + encrypted_private_post_states, + new_commitments: output.new_commitments, + new_nullifiers: output.new_nullifiers, + }) + } +} + +#[cfg(test)] +pub mod tests { + use std::io::Cursor; + + use nssa_core::{Commitment, Nullifier, NullifierPublicKey, account::Account}; + + use crate::{Address, privacy_preserving_transaction::message::Message}; + + pub fn message_for_tests() -> Message { + let account1 = Account::default(); + let account2 = Account::default(); + + let nsk1 = [11; 32]; + let nsk2 = [12; 32]; + + let npk1 = NullifierPublicKey::from(&nsk1); + let npk2 = NullifierPublicKey::from(&nsk2); + + let public_addresses = vec![Address::new([1; 32])]; + + let nonces = vec![1, 2, 3]; + + let public_post_states = vec![Account::default()]; + + let encrypted_private_post_states = Vec::new(); + + let new_commitments = vec![Commitment::new(&npk2, &account2)]; + + let old_commitment = Commitment::new(&npk1, &account1); + let new_nullifiers = vec![(Nullifier::new(&old_commitment, &nsk1), [0; 32])]; + + Message { + public_addresses: public_addresses.clone(), + nonces: nonces.clone(), + public_post_states: public_post_states.clone(), + encrypted_private_post_states: encrypted_private_post_states.clone(), + new_commitments: new_commitments.clone(), + new_nullifiers: new_nullifiers.clone(), + } + } + + #[test] + fn test_message_serialization_roundtrip() { + let message = message_for_tests(); + + let bytes = message.to_bytes(); + let mut cursor = Cursor::new(bytes.as_ref()); + let message_from_cursor = Message::from_cursor(&mut cursor).unwrap(); + + assert_eq!(message, message_from_cursor); + } +} diff --git a/nssa/src/privacy_preserving_transaction/mod.rs b/nssa/src/privacy_preserving_transaction/mod.rs new file mode 100644 index 0000000..54fc94b --- /dev/null +++ b/nssa/src/privacy_preserving_transaction/mod.rs @@ -0,0 +1,8 @@ +mod encoding; +pub mod message; +pub mod transaction; +pub mod witness_set; + +pub mod circuit; + +pub use transaction::PrivacyPreservingTransaction; diff --git a/nssa/src/privacy_preserving_transaction/transaction.rs b/nssa/src/privacy_preserving_transaction/transaction.rs new file mode 100644 index 0000000..9aac54e --- /dev/null +++ b/nssa/src/privacy_preserving_transaction/transaction.rs @@ -0,0 +1,169 @@ +use std::collections::{HashMap, HashSet}; + +use nssa_core::{ + Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput, + account::{Account, AccountWithMetadata}, +}; + +use crate::error::NssaError; +use crate::privacy_preserving_transaction::circuit::Proof; +use crate::privacy_preserving_transaction::message::EncryptedAccountData; +use crate::{Address, V01State}; + +use super::message::Message; +use super::witness_set::WitnessSet; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PrivacyPreservingTransaction { + message: Message, + witness_set: WitnessSet, +} + +impl PrivacyPreservingTransaction { + pub fn new(message: Message, witness_set: WitnessSet) -> Self { + Self { + message, + witness_set, + } + } + + pub(crate) fn validate_and_produce_public_state_diff( + &self, + state: &mut V01State, + ) -> Result, NssaError> { + let message = &self.message; + let witness_set = &self.witness_set; + + // 1. Commitments or nullifiers are non empty + if message.new_commitments.is_empty() && message.new_nullifiers.is_empty() { + return Err(NssaError::InvalidInput( + "Empty commitments and empty nullifiers found in message".into(), + )); + } + + // 2. Check there are no duplicate addresses in the public_addresses list. + if n_unique(&message.public_addresses) != message.public_addresses.len() { + return Err(NssaError::InvalidInput( + "Duplicate addresses found in message".into(), + )); + } + + // Check there are no duplicate nullifiers in the new_nullifiers list + if n_unique(&message.new_nullifiers) != message.new_nullifiers.len() { + return Err(NssaError::InvalidInput( + "Duplicate nullifiers found in message".into(), + )); + } + + // Check there are no duplicate commitments in the new_commitments list + if n_unique(&message.new_commitments) != message.new_commitments.len() { + return Err(NssaError::InvalidInput( + "Duplicate commitments found in message".into(), + )); + } + + // 3. Nonce checks and Valid signatures + // Check exactly one nonce is provided for each signature + 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.signatures_are_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 proof verification + let public_pre_states: Vec<_> = message + .public_addresses + .iter() + .map(|address| AccountWithMetadata { + account: state.get_account_by_address(address), + is_authorized: signer_addresses.contains(address), + }) + .collect(); + + // 4. Proof verification + check_privacy_preserving_circuit_proof_is_valid( + &witness_set.proof, + &public_pre_states, + &message.public_post_states, + &message.encrypted_private_post_states, + &message.new_commitments, + &message.new_nullifiers, + )?; + + // 5. Commitment freshness + state.check_commitments_are_new(&message.new_commitments)?; + + // 6. Nullifier uniqueness + state.check_nullifiers_are_valid(&message.new_nullifiers)?; + + Ok(message + .public_addresses + .iter() + .cloned() + .zip(message.public_post_states.clone()) + .collect()) + } + + 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)) + .collect() + } +} + +fn check_privacy_preserving_circuit_proof_is_valid( + proof: &Proof, + public_pre_states: &[AccountWithMetadata], + public_post_states: &[Account], + encrypted_private_post_states: &[EncryptedAccountData], + new_commitments: &[Commitment], + new_nullifiers: &[(Nullifier, CommitmentSetDigest)], +) -> Result<(), NssaError> { + let output = PrivacyPreservingCircuitOutput { + public_pre_states: public_pre_states.to_vec(), + public_post_states: public_post_states.to_vec(), + ciphertexts: encrypted_private_post_states + .iter() + .cloned() + .map(|value| value.ciphertext) + .collect(), + new_commitments: new_commitments.to_vec(), + new_nullifiers: new_nullifiers.to_vec(), + }; + proof + .is_valid_for(&output) + .then_some(()) + .ok_or(NssaError::InvalidPrivacyPreservingProof) +} + +use std::hash::Hash; +fn n_unique(data: &[T]) -> usize { + let set: HashSet<&T> = data.iter().collect(); + set.len() +} diff --git a/nssa/src/privacy_preserving_transaction/witness_set.rs b/nssa/src/privacy_preserving_transaction/witness_set.rs new file mode 100644 index 0000000..fe897ce --- /dev/null +++ b/nssa/src/privacy_preserving_transaction/witness_set.rs @@ -0,0 +1,47 @@ +use crate::{ + PrivateKey, PublicKey, Signature, + privacy_preserving_transaction::{circuit::Proof, message::Message}, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WitnessSet { + pub(super) signatures_and_public_keys: Vec<(Signature, PublicKey)>, + pub(super) proof: Proof, +} + +impl WitnessSet { + pub fn for_message(message: &Message, proof: Proof, private_keys: &[&PrivateKey]) -> Self { + let message_bytes = message.to_bytes(); + let signatures_and_public_keys = private_keys + .iter() + .map(|&key| { + ( + Signature::new(key, &message_bytes), + PublicKey::new_from_private_key(key), + ) + }) + .collect(); + Self { + proof, + signatures_and_public_keys, + } + } + + pub fn signatures_are_valid_for(&self, message: &Message) -> bool { + let message_bytes = message.to_bytes(); + for (signature, public_key) in self.signatures_and_public_keys() { + if !signature.is_valid_for(&message_bytes, public_key) { + return false; + } + } + true + } + + pub fn signatures_and_public_keys(&self) -> &[(Signature, PublicKey)] { + &self.signatures_and_public_keys + } + + pub fn proof(&self) -> &Proof { + &self.proof + } +} diff --git a/nssa/src/program.rs b/nssa/src/program.rs index e2762ba..66358e9 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -1,6 +1,6 @@ use nssa_core::{ account::{Account, AccountWithMetadata}, - program::{DEFAULT_PROGRAM_ID, InstructionData, ProgramId}, + program::{InstructionData, ProgramId, ProgramOutput}, }; use program_methods::{AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID}; use risc0_zkvm::{ExecutorEnv, ExecutorEnvBuilder, default_executor, serde::to_vec}; @@ -19,6 +19,10 @@ impl Program { self.id } + pub(crate) fn elf(&self) -> &'static [u8] { + self.elf + } + pub fn serialize_instruction( instruction: T, ) -> Result { @@ -42,23 +46,16 @@ impl Program { .map_err(|e| NssaError::ProgramExecutionFailed(e.to_string()))?; // Get outputs - let mut post_states: Vec = session_info + let ProgramOutput { post_states, .. } = session_info .journal .decode() .map_err(|e| NssaError::ProgramExecutionFailed(e.to_string()))?; - // Claim any output account with default program owner field - for account in post_states.iter_mut() { - if account.program_owner == DEFAULT_PROGRAM_ID { - account.program_owner = self.id; - } - } - Ok(post_states) } /// Writes inputs to `env_builder` in the order expected by the programs - fn write_inputs( + pub(crate) fn write_inputs( pre_states: &[AccountWithMetadata], instruction_data: &[u32], env_builder: &mut ExecutorEnvBuilder, @@ -66,7 +63,7 @@ impl Program { let pre_states = pre_states.to_vec(); env_builder .write(&(pre_states, instruction_data)) - .map_err(|e| NssaError::ProgramExecutionFailed(e.to_string()))?; + .map_err(|e| NssaError::ProgramWriteInputFailed(e.to_string()))?; Ok(()) } @@ -185,13 +182,10 @@ mod tests { let expected_sender_post = Account { balance: 77665544332211 - balance_to_move, - program_owner: program.id(), ..Account::default() }; let expected_recipient_post = Account { balance: balance_to_move, - // Program claims the account since the pre_state has default prorgam owner - program_owner: program.id(), ..Account::default() }; let [sender_post, recipient_post] = program diff --git a/nssa/src/public_transaction/encoding.rs b/nssa/src/public_transaction/encoding.rs index 3c004a0..e8890de 100644 --- a/nssa/src/public_transaction/encoding.rs +++ b/nssa/src/public_transaction/encoding.rs @@ -1,3 +1,5 @@ +// TODO: Consider switching to deriving Borsh + use std::io::{Cursor, Read}; use nssa_core::program::ProgramId; @@ -8,8 +10,8 @@ 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 = 22; +const MESSAGE_ENCODING_PREFIX: &[u8; MESSAGE_ENCODING_PREFIX_LEN] = b"\x00/NSSA/v0.1/TxMessage/"; impl Message { /// Serializes a `Message` into bytes in the following layout: @@ -52,7 +54,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/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index d160004..20b2729 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -50,7 +50,7 @@ impl PublicTransaction { hasher.finalize_fixed().into() } - pub(crate) fn validate_and_compute_post_states( + pub(crate) fn validate_and_produce_public_state_diff( &self, state: &V01State, ) -> Result, NssaError> { @@ -64,6 +64,7 @@ impl PublicTransaction { )); } + // Check exactly one nonce is provided for each signature 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(), @@ -232,7 +233,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_compute_post_states(&state); + let result = tx.validate_and_produce_public_state_diff(&state); assert!(matches!(result, Err(NssaError::InvalidInput(_)))) } @@ -252,7 +253,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_compute_post_states(&state); + let result = tx.validate_and_produce_public_state_diff(&state); assert!(matches!(result, Err(NssaError::InvalidInput(_)))) } @@ -273,7 +274,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_compute_post_states(&state); + let result = tx.validate_and_produce_public_state_diff(&state); assert!(matches!(result, Err(NssaError::InvalidInput(_)))) } @@ -293,7 +294,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_compute_post_states(&state); + let result = tx.validate_and_produce_public_state_diff(&state); assert!(matches!(result, Err(NssaError::InvalidInput(_)))) } @@ -309,7 +310,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_compute_post_states(&state); + let result = tx.validate_and_produce_public_state_diff(&state); assert!(matches!(result, Err(NssaError::InvalidInput(_)))) } } diff --git a/nssa/src/state.rs b/nssa/src/state.rs index fc24355..93ee06c 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -1,11 +1,60 @@ use crate::{ - address::Address, error::NssaError, program::Program, public_transaction::PublicTransaction, + address::Address, error::NssaError, merkle_tree::MerkleTree, + privacy_preserving_transaction::PrivacyPreservingTransaction, program::Program, + public_transaction::PublicTransaction, }; -use nssa_core::{account::Account, program::ProgramId}; -use std::collections::HashMap; +use nssa_core::{ + Commitment, CommitmentSetDigest, MembershipProof, Nullifier, + account::Account, + program::{DEFAULT_PROGRAM_ID, ProgramId}, +}; +use std::collections::{HashMap, HashSet}; + +pub(crate) struct CommitmentSet { + merkle_tree: MerkleTree, + commitments: HashMap, + root_history: HashSet, +} + +impl CommitmentSet { + pub(crate) fn digest(&self) -> CommitmentSetDigest { + self.merkle_tree.root() + } + + pub fn get_proof_for(&self, commitment: &Commitment) -> Option { + let index = *self.commitments.get(commitment)?; + + self.merkle_tree + .get_authentication_path_for(index) + .map(|path| (index, path)) + } + + pub(crate) fn extend(&mut self, commitments: &[Commitment]) { + for commitment in commitments.iter().cloned() { + let index = self.merkle_tree.insert(commitment.to_byte_array()); + self.commitments.insert(commitment, index); + } + self.root_history.insert(self.digest()); + } + + fn contains(&self, commitment: &Commitment) -> bool { + self.commitments.contains_key(commitment) + } + + pub(crate) fn with_capacity(capacity: usize) -> CommitmentSet { + Self { + merkle_tree: MerkleTree::with_capacity(capacity), + commitments: HashMap::new(), + root_history: HashSet::new(), + } + } +} + +type NullifierSet = HashSet; pub struct V01State { public_state: HashMap, + private_state: (CommitmentSet, NullifierSet), builtin_programs: HashMap, } @@ -27,6 +76,7 @@ impl V01State { let mut this = Self { public_state, + private_state: (CommitmentSet::with_capacity(32), NullifierSet::new()), builtin_programs: HashMap::new(), }; @@ -43,13 +93,54 @@ impl V01State { &mut self, tx: &PublicTransaction, ) -> Result<(), NssaError> { - let state_diff = tx.validate_and_compute_post_states(self)?; + let state_diff = tx.validate_and_produce_public_state_diff(self)?; for (address, post) in state_diff.into_iter() { let current_account = self.get_account_by_address_mut(address); + + *current_account = post; + // The invoked program claims the accounts with default program id. + if current_account.program_owner == DEFAULT_PROGRAM_ID { + current_account.program_owner = tx.message().program_id; + } + } + + for address in tx.signer_addresses() { + let current_account = self.get_account_by_address_mut(address); + current_account.nonce += 1; + } + + Ok(()) + } + + pub fn transition_from_privacy_preserving_transaction( + &mut self, + tx: &PrivacyPreservingTransaction, + ) -> Result<(), NssaError> { + // 1. Verify the transaction satisfies acceptance criteria + let public_state_diff = tx.validate_and_produce_public_state_diff(self)?; + + let message = tx.message(); + + // 2. Add new commitments + self.private_state.0.extend(&message.new_commitments); + + // 3. Add new nullifiers + let new_nullifiers = message + .new_nullifiers + .iter() + .cloned() + .map(|(nullifier, _)| nullifier) + .collect::>(); + self.private_state.1.extend(new_nullifiers); + + // 4. Update public accounts + for (address, post) in public_state_diff.into_iter() { + let current_account = self.get_account_by_address_mut(address); *current_account = post; } + // // 5. Increment nonces for address in tx.signer_addresses() { let current_account = self.get_account_by_address_mut(address); current_account.nonce += 1; @@ -69,21 +160,73 @@ impl V01State { .unwrap_or(Account::default()) } + pub fn get_proof_for_commitment(&self, commitment: &Commitment) -> Option { + self.private_state.0.get_proof_for(commitment) + } + pub(crate) fn builtin_programs(&self) -> &HashMap { &self.builtin_programs } + + pub fn commitment_set_digest(&self) -> CommitmentSetDigest { + self.private_state.0.digest() + } + + pub(crate) fn check_commitments_are_new( + &self, + new_commitments: &[Commitment], + ) -> Result<(), NssaError> { + 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_valid( + &self, + new_nullifiers: &[(Nullifier, CommitmentSetDigest)], + ) -> Result<(), NssaError> { + for (nullifier, digest) in new_nullifiers.iter() { + if self.private_state.1.contains(nullifier) { + return Err(NssaError::InvalidInput( + "Nullifier already seen".to_string(), + )); + } + if !self.private_state.0.root_history.contains(digest) { + return Err(NssaError::InvalidInput( + "Unrecognized commitment set digest".to_string(), + )); + } + } + Ok(()) + } } #[cfg(test)] -mod tests { +pub mod tests { use std::collections::HashMap; use crate::{ - Address, PublicKey, PublicTransaction, V01State, error::NssaError, program::Program, - public_transaction, signature::PrivateKey, + Address, PublicKey, PublicTransaction, V01State, + error::NssaError, + privacy_preserving_transaction::{ + PrivacyPreservingTransaction, circuit, message::Message, witness_set::WitnessSet, + }, + program::Program, + public_transaction, + signature::PrivateKey, + }; + + use nssa_core::{ + Commitment, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, + account::{Account, AccountWithMetadata, Nonce}, + encryption::{EphemeralPublicKey, IncomingViewingPublicKey}, }; - use nssa_core::account::Account; fn transfer_transaction( from: Address, @@ -331,6 +474,12 @@ mod tests { self.force_insert_account(Address::new([252; 32]), account); self } + + pub fn with_private_account(mut self, keys: &TestPrivateKeys, account: &Account) -> Self { + let commitment = Commitment::new(&keys.npk(), account); + self.private_state.0.extend(&[commitment]); + self + } } #[test] @@ -570,4 +719,348 @@ mod tests { assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); } + + pub struct TestPublicKeys { + pub signing_key: PrivateKey, + } + + impl TestPublicKeys { + pub fn address(&self) -> Address { + Address::from(&PublicKey::new_from_private_key(&self.signing_key)) + } + } + + fn test_public_account_keys_1() -> TestPublicKeys { + TestPublicKeys { + signing_key: PrivateKey::try_new([37; 32]).unwrap(), + } + } + + pub struct TestPrivateKeys { + pub nsk: NullifierSecretKey, + pub isk: [u8; 32], + } + + impl TestPrivateKeys { + pub fn npk(&self) -> NullifierPublicKey { + NullifierPublicKey::from(&self.nsk) + } + + pub fn ivk(&self) -> IncomingViewingPublicKey { + IncomingViewingPublicKey::from_scalar(self.isk) + } + } + + pub fn test_private_account_keys_1() -> TestPrivateKeys { + TestPrivateKeys { + nsk: [13; 32], + isk: [31; 32], + } + } + + pub fn test_private_account_keys_2() -> TestPrivateKeys { + TestPrivateKeys { + nsk: [38; 32], + isk: [83; 32], + } + } + + fn shielded_balance_transfer_for_tests( + sender_keys: &TestPublicKeys, + recipient_keys: &TestPrivateKeys, + balance_to_move: u128, + state: &V01State, + ) -> PrivacyPreservingTransaction { + let sender = AccountWithMetadata { + account: state.get_account_by_address(&sender_keys.address()), + is_authorized: true, + }; + + let sender_nonce = sender.account.nonce; + + let recipient = AccountWithMetadata { + account: Account::default(), + is_authorized: false, + }; + + let esk = [3; 32]; + let shared_secret = SharedSecretKey::new(&esk, &recipient_keys.ivk()); + let epk = EphemeralPublicKey::from_scalar(esk); + + let (output, proof) = circuit::execute_and_prove( + &[sender, recipient], + &Program::serialize_instruction(balance_to_move).unwrap(), + &[0, 2], + &[0xdeadbeef], + &[(recipient_keys.npk(), shared_secret)], + &[], + &Program::authenticated_transfer_program(), + ) + .unwrap(); + + let message = Message::try_from_circuit_output( + vec![sender_keys.address()], + vec![sender_nonce], + vec![epk], + output, + ) + .unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[&sender_keys.signing_key]); + PrivacyPreservingTransaction::new(message, witness_set) + } + + fn private_balance_transfer_for_tests( + sender_keys: &TestPrivateKeys, + sender_private_account: &Account, + recipient_keys: &TestPrivateKeys, + balance_to_move: u128, + new_nonces: [Nonce; 2], + state: &V01State, + ) -> PrivacyPreservingTransaction { + let program = Program::authenticated_transfer_program(); + let sender_commitment = Commitment::new(&sender_keys.npk(), sender_private_account); + let sender_pre = AccountWithMetadata { + account: sender_private_account.clone(), + is_authorized: true, + }; + let recipient_pre = AccountWithMetadata { + account: Account::default(), + is_authorized: false, + }; + + let esk_1 = [3; 32]; + let shared_secret_1 = SharedSecretKey::new(&esk_1, &sender_keys.ivk()); + let epk_1 = EphemeralPublicKey::from_scalar(esk_1); + + let esk_2 = [3; 32]; + let shared_secret_2 = SharedSecretKey::new(&esk_2, &recipient_keys.ivk()); + let epk_2 = EphemeralPublicKey::from_scalar(esk_2); + + let (output, proof) = circuit::execute_and_prove( + &[sender_pre, recipient_pre], + &Program::serialize_instruction(balance_to_move).unwrap(), + &[1, 2], + &new_nonces, + &[ + (sender_keys.npk(), shared_secret_1), + (recipient_keys.npk(), shared_secret_2), + ], + &[( + sender_keys.nsk, + state.get_proof_for_commitment(&sender_commitment).unwrap(), + )], + &program, + ) + .unwrap(); + + let message = + Message::try_from_circuit_output(vec![], vec![], vec![epk_1, epk_2], output).unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[]); + + PrivacyPreservingTransaction::new(message, witness_set) + } + + fn deshielded_balance_transfer_for_tests( + sender_keys: &TestPrivateKeys, + sender_private_account: &Account, + recipient_address: &Address, + balance_to_move: u128, + new_nonce: Nonce, + state: &V01State, + ) -> PrivacyPreservingTransaction { + let program = Program::authenticated_transfer_program(); + let sender_commitment = Commitment::new(&sender_keys.npk(), sender_private_account); + let sender_pre = AccountWithMetadata { + account: sender_private_account.clone(), + is_authorized: true, + }; + let recipient_pre = AccountWithMetadata { + account: state.get_account_by_address(recipient_address), + is_authorized: false, + }; + + let esk = [3; 32]; + let shared_secret = SharedSecretKey::new(&esk, &sender_keys.ivk()); + let epk = EphemeralPublicKey::from_scalar(esk); + + let (output, proof) = circuit::execute_and_prove( + &[sender_pre, recipient_pre], + &Program::serialize_instruction(balance_to_move).unwrap(), + &[1, 0], + &[new_nonce], + &[(sender_keys.npk(), shared_secret)], + &[( + sender_keys.nsk, + state.get_proof_for_commitment(&sender_commitment).unwrap(), + )], + &program, + ) + .unwrap(); + + let message = + Message::try_from_circuit_output(vec![*recipient_address], vec![], vec![epk], output) + .unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[]); + + PrivacyPreservingTransaction::new(message, witness_set) + } + + #[test] + fn test_transition_from_privacy_preserving_transaction_shielded() { + let sender_keys = test_public_account_keys_1(); + let recipient_keys = test_private_account_keys_1(); + + let mut state = V01State::new_with_genesis_accounts(&[(sender_keys.address(), 200)]); + + let balance_to_move = 37; + + let tx = shielded_balance_transfer_for_tests( + &sender_keys, + &recipient_keys, + balance_to_move, + &state, + ); + + let [expected_new_commitment] = tx.message().new_commitments.clone().try_into().unwrap(); + assert!(!state.private_state.0.contains(&expected_new_commitment)); + + state + .transition_from_privacy_preserving_transaction(&tx) + .unwrap(); + + assert!(state.private_state.0.contains(&expected_new_commitment)); + + assert_eq!( + state.get_account_by_address(&sender_keys.address()).balance, + 200 - balance_to_move + ); + } + + #[test] + fn test_transition_from_privacy_preserving_transaction_private() { + let sender_keys = test_private_account_keys_1(); + let sender_private_account = Account { + program_owner: Program::authenticated_transfer_program().id(), + balance: 100, + nonce: 0xdeadbeef, + data: vec![], + }; + let recipient_keys = test_private_account_keys_2(); + + let mut state = V01State::new_with_genesis_accounts(&[]) + .with_private_account(&sender_keys, &sender_private_account); + + let balance_to_move = 37; + + let tx = private_balance_transfer_for_tests( + &sender_keys, + &sender_private_account, + &recipient_keys, + balance_to_move, + [0xcafecafe, 0xfecafeca], + &state, + ); + + let expected_new_commitment_1 = Commitment::new( + &sender_keys.npk(), + &Account { + program_owner: Program::authenticated_transfer_program().id(), + nonce: 0xcafecafe, + balance: sender_private_account.balance - balance_to_move, + data: vec![], + }, + ); + + let sender_pre_commitment = Commitment::new(&sender_keys.npk(), &sender_private_account); + let expected_new_nullifier = Nullifier::new(&sender_pre_commitment, &sender_keys.nsk); + + let expected_new_commitment_2 = Commitment::new( + &recipient_keys.npk(), + &Account { + program_owner: Program::authenticated_transfer_program().id(), + nonce: 0xfecafeca, + balance: balance_to_move, + ..Account::default() + }, + ); + + let previous_public_state = state.public_state.clone(); + assert!(state.private_state.0.contains(&sender_pre_commitment)); + assert!(!state.private_state.0.contains(&expected_new_commitment_1)); + assert!(!state.private_state.0.contains(&expected_new_commitment_2)); + assert!(!state.private_state.1.contains(&expected_new_nullifier)); + + state + .transition_from_privacy_preserving_transaction(&tx) + .unwrap(); + + assert_eq!(state.public_state, previous_public_state); + assert!(state.private_state.0.contains(&sender_pre_commitment)); + assert!(state.private_state.0.contains(&expected_new_commitment_1)); + assert!(state.private_state.0.contains(&expected_new_commitment_2)); + assert!(state.private_state.1.contains(&expected_new_nullifier)); + } + + #[test] + fn test_transition_from_privacy_preserving_transaction_deshielded() { + let sender_keys = test_private_account_keys_1(); + let sender_private_account = Account { + program_owner: Program::authenticated_transfer_program().id(), + balance: 100, + nonce: 0xdeadbeef, + data: vec![], + }; + let recipient_keys = test_public_account_keys_1(); + let recipient_initial_balance = 400; + let mut state = V01State::new_with_genesis_accounts(&[( + recipient_keys.address(), + recipient_initial_balance, + )]) + .with_private_account(&sender_keys, &sender_private_account); + + let balance_to_move = 37; + + let tx = deshielded_balance_transfer_for_tests( + &sender_keys, + &sender_private_account, + &recipient_keys.address(), + balance_to_move, + 0xcafecafe, + &state, + ); + + let expected_new_commitment = Commitment::new( + &sender_keys.npk(), + &Account { + program_owner: Program::authenticated_transfer_program().id(), + nonce: 0xcafecafe, + balance: sender_private_account.balance - balance_to_move, + data: vec![], + }, + ); + + let sender_pre_commitment = Commitment::new(&sender_keys.npk(), &sender_private_account); + let expected_new_nullifier = Nullifier::new(&sender_pre_commitment, &sender_keys.nsk); + + assert!(state.private_state.0.contains(&sender_pre_commitment)); + assert!(!state.private_state.0.contains(&expected_new_commitment)); + assert!(!state.private_state.1.contains(&expected_new_nullifier)); + + state + .transition_from_privacy_preserving_transaction(&tx) + .unwrap(); + + assert!(state.private_state.0.contains(&sender_pre_commitment)); + assert!(state.private_state.0.contains(&expected_new_commitment)); + assert!(state.private_state.1.contains(&expected_new_nullifier)); + assert_eq!( + state + .get_account_by_address(&recipient_keys.address()) + .balance, + recipient_initial_balance + balance_to_move + ); + } } diff --git a/nssa/test_program_methods/guest/src/bin/burner.rs b/nssa/test_program_methods/guest/src/bin/burner.rs index 018c203..1ef7373 100644 --- a/nssa/test_program_methods/guest/src/bin/burner.rs +++ b/nssa/test_program_methods/guest/src/bin/burner.rs @@ -1,19 +1,21 @@ -use nssa_core::program::read_nssa_inputs; -use risc0_zkvm::guest::env; +use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput}; type Instruction = u128; fn main() { - let (input_accounts, balance_to_burn) = read_nssa_inputs::(); + let ProgramInput { + pre_states, + instruction: balance_to_burn, + } = read_nssa_inputs::(); - let [pre] = match input_accounts.try_into() { + let [pre] = match pre_states.try_into() { Ok(array) => array, Err(_) => return, }; - let account_pre = pre.account; + let account_pre = &pre.account; let mut account_post = account_pre.clone(); account_post.balance -= balance_to_burn; - env::commit(&vec![account_post]); + write_nssa_outputs(vec![pre], vec![account_post]); } diff --git a/nssa/test_program_methods/guest/src/bin/data_changer.rs b/nssa/test_program_methods/guest/src/bin/data_changer.rs index fa1efd3..c7d34a2 100644 --- a/nssa/test_program_methods/guest/src/bin/data_changer.rs +++ b/nssa/test_program_methods/guest/src/bin/data_changer.rs @@ -1,20 +1,18 @@ -use nssa_core::program::read_nssa_inputs; -use risc0_zkvm::guest::env; +use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput}; type Instruction = (); fn main() { - let (input_accounts, _) = read_nssa_inputs::(); + let ProgramInput { pre_states, .. } = read_nssa_inputs::(); - let [pre] = match input_accounts.try_into() { + let [pre] = match pre_states.try_into() { Ok(array) => array, Err(_) => return, }; - let account_pre = pre.account; + let account_pre = &pre.account; let mut account_post = account_pre.clone(); account_post.data.push(0); - env::commit(&vec![account_post]); + write_nssa_outputs(vec![pre], vec![account_post]); } - diff --git a/nssa/test_program_methods/guest/src/bin/extra_output.rs b/nssa/test_program_methods/guest/src/bin/extra_output.rs index c02c5e2..3543d51 100644 --- a/nssa/test_program_methods/guest/src/bin/extra_output.rs +++ b/nssa/test_program_methods/guest/src/bin/extra_output.rs @@ -1,17 +1,19 @@ -use nssa_core::{account::Account, program::read_nssa_inputs}; -use risc0_zkvm::guest::env; +use nssa_core::{ + account::Account, + program::{read_nssa_inputs, write_nssa_outputs, ProgramInput}, +}; type Instruction = (); fn main() { - let (input_accounts, _) = read_nssa_inputs::(); + let ProgramInput { pre_states, .. } = read_nssa_inputs::(); - let [pre] = match input_accounts.try_into() { + let [pre] = match pre_states.try_into() { Ok(array) => array, Err(_) => return, }; - let account_pre = pre.account; + let account_pre = pre.account.clone(); - env::commit(&vec![account_pre, Account::default()]); + write_nssa_outputs(vec![pre], vec![account_pre, Account::default()]); } diff --git a/nssa/test_program_methods/guest/src/bin/minter.rs b/nssa/test_program_methods/guest/src/bin/minter.rs index b82d9e9..2ec97a9 100644 --- a/nssa/test_program_methods/guest/src/bin/minter.rs +++ b/nssa/test_program_methods/guest/src/bin/minter.rs @@ -1,19 +1,18 @@ -use nssa_core::program::read_nssa_inputs; -use risc0_zkvm::guest::env; +use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput}; type Instruction = (); fn main() { - let (input_accounts, _) = read_nssa_inputs::(); + let ProgramInput { pre_states, .. } = read_nssa_inputs::(); - let [pre] = match input_accounts.try_into() { + let [pre] = match pre_states.try_into() { Ok(array) => array, Err(_) => return, }; - let account_pre = pre.account; + let account_pre = &pre.account; let mut account_post = account_pre.clone(); account_post.balance += 1; - env::commit(&vec![account_post]); + write_nssa_outputs(vec![pre], vec![account_post]); } diff --git a/nssa/test_program_methods/guest/src/bin/missing_output.rs b/nssa/test_program_methods/guest/src/bin/missing_output.rs index 61aa8c5..2174266 100644 --- a/nssa/test_program_methods/guest/src/bin/missing_output.rs +++ b/nssa/test_program_methods/guest/src/bin/missing_output.rs @@ -1,17 +1,16 @@ -use nssa_core::program::read_nssa_inputs; -use risc0_zkvm::guest::env; +use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput}; type Instruction = (); fn main() { - let (input_accounts, _) = read_nssa_inputs::(); + let ProgramInput { pre_states, .. } = read_nssa_inputs::(); - let [pre1, _] = match input_accounts.try_into() { + let [pre1, _] = match pre_states.try_into() { Ok(array) => array, Err(_) => return, }; - let account_pre1 = pre1.account; + let account_pre1 = pre1.account.clone(); - env::commit(&vec![account_pre1]); + write_nssa_outputs(vec![pre1], vec![account_pre1]); } diff --git a/nssa/test_program_methods/guest/src/bin/nonce_changer.rs b/nssa/test_program_methods/guest/src/bin/nonce_changer.rs index eb1365e..b3b2599 100644 --- a/nssa/test_program_methods/guest/src/bin/nonce_changer.rs +++ b/nssa/test_program_methods/guest/src/bin/nonce_changer.rs @@ -1,19 +1,18 @@ -use nssa_core::program::read_nssa_inputs; -use risc0_zkvm::guest::env; +use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput}; type Instruction = (); fn main() { - let (input_accounts, _) = read_nssa_inputs::(); + let ProgramInput { pre_states, .. } = read_nssa_inputs::(); - let [pre] = match input_accounts.try_into() { + let [pre] = match pre_states.try_into() { Ok(array) => array, Err(_) => return, }; - let account_pre = pre.account; + let account_pre = &pre.account; let mut account_post = account_pre.clone(); account_post.nonce += 1; - env::commit(&vec![account_post]); + write_nssa_outputs(vec![pre], vec![account_post]); } diff --git a/nssa/test_program_methods/guest/src/bin/program_owner_changer.rs b/nssa/test_program_methods/guest/src/bin/program_owner_changer.rs index 4d11438..49947cd 100644 --- a/nssa/test_program_methods/guest/src/bin/program_owner_changer.rs +++ b/nssa/test_program_methods/guest/src/bin/program_owner_changer.rs @@ -1,19 +1,18 @@ -use nssa_core::program::read_nssa_inputs; -use risc0_zkvm::guest::env; +use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput}; type Instruction = (); fn main() { - let (input_accounts, _) = read_nssa_inputs::(); + let ProgramInput { pre_states, .. } = read_nssa_inputs::(); - let [pre] = match input_accounts.try_into() { + let [pre] = match pre_states.try_into() { Ok(array) => array, Err(_) => return, }; - let account_pre = pre.account; + let account_pre = &pre.account; let mut account_post = account_pre.clone(); account_post.program_owner = [0, 1, 2, 3, 4, 5, 6, 7]; - env::commit(&vec![account_post]); + write_nssa_outputs(vec![pre], vec![account_post]); } diff --git a/nssa/test_program_methods/guest/src/bin/simple_balance_transfer.rs b/nssa/test_program_methods/guest/src/bin/simple_balance_transfer.rs index 047e252..13263c5 100644 --- a/nssa/test_program_methods/guest/src/bin/simple_balance_transfer.rs +++ b/nssa/test_program_methods/guest/src/bin/simple_balance_transfer.rs @@ -1,12 +1,14 @@ -use nssa_core::program::read_nssa_inputs; -use risc0_zkvm::guest::env; +use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput}; type Instruction = u128; fn main() { - let (input_accounts, balance) = read_nssa_inputs::(); + let ProgramInput { + pre_states, + instruction: balance, + } = read_nssa_inputs::(); - let [sender_pre, receiver_pre] = match input_accounts.try_into() { + let [sender_pre, receiver_pre] = match pre_states.try_into() { Ok(array) => array, Err(_) => return, }; @@ -16,5 +18,8 @@ fn main() { sender_post.balance -= balance; receiver_post.balance += balance; - env::commit(&vec![sender_post, receiver_post]); + write_nssa_outputs( + vec![sender_pre, receiver_pre], + vec![sender_post, receiver_post], + ); }