diff --git a/nssa/Cargo.toml b/nssa/Cargo.toml index 19e751a..f479999 100644 --- a/nssa/Cargo.toml +++ b/nssa/Cargo.toml @@ -12,6 +12,7 @@ serde = "1.0.219" sha2 = "0.10.9" secp256k1 = "0.31.1" rand = "0.8" +borsh = "1.5.7" [dev-dependencies] test-program-methods = { path = "test_program_methods" } diff --git a/nssa/core/src/account/encoding.rs b/nssa/core/src/account/encoding.rs index ae96ebe..5b1dd0e 100644 --- a/nssa/core/src/account/encoding.rs +++ b/nssa/core/src/account/encoding.rs @@ -1,3 +1,5 @@ +// TODO: Consider switching to deriving Borsh + use risc0_zkvm::sha::{Impl, Sha256}; #[cfg(feature = "host")] diff --git a/nssa/core/src/account/mod.rs b/nssa/core/src/account/mod.rs index 1358eb1..2bb2285 100644 --- a/nssa/core/src/account/mod.rs +++ b/nssa/core/src/account/mod.rs @@ -3,8 +3,8 @@ use serde::{Deserialize, Serialize}; use crate::program::ProgramId; mod commitment; -mod nullifier; mod encoding; +mod nullifier; pub use commitment::Commitment; pub use nullifier::{Nullifier, NullifierPublicKey, NullifierSecretKey}; @@ -22,7 +22,7 @@ pub struct Account { pub nonce: Nonce, } -#[derive(Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct AccountWithMetadata { pub account: Account, pub is_authorized: bool, diff --git a/nssa/core/src/error.rs b/nssa/core/src/error.rs index b9e5020..8f05323 100644 --- a/nssa/core/src/error.rs +++ b/nssa/core/src/error.rs @@ -4,7 +4,7 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum NssaCoreError { - #[error("Invalid transaction: {0}")] + #[error("Deserialization error: {0}")] DeserializationError(String), #[error("IO error: {0}")] diff --git a/nssa/core/src/lib.rs b/nssa/core/src/lib.rs index a247d19..bd01216 100644 --- a/nssa/core/src/lib.rs +++ b/nssa/core/src/lib.rs @@ -1,7 +1,9 @@ +use risc0_zkvm::serde::to_vec; use serde::{Deserialize, Serialize}; #[cfg(feature = "host")] use crate::error::NssaCoreError; + use crate::{ account::{ Account, AccountWithMetadata, Commitment, Nonce, Nullifier, NullifierPublicKey, @@ -78,8 +80,8 @@ impl EncryptedAccountData { pub struct PrivacyPreservingCircuitInput { pub program_output: ProgramOutput, pub visibility_mask: Vec, - pub private_account_data: Vec<( - Nonce, + pub private_account_nonces: Vec, + pub private_account_keys: Vec<( NullifierPublicKey, IncomingViewingPublicKey, EphemeralSecretKey, @@ -98,3 +100,4 @@ pub struct PrivacyPreservingCircuitOutput { pub new_nullifiers: Vec, pub commitment_set_digest: CommitmentSetDigest, } + diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index ff8fd57..f5615e9 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -3,6 +3,9 @@ use risc0_zkvm::serde::Deserializer; use risc0_zkvm::{DeserializeOwned, guest::env}; use serde::{Deserialize, Serialize}; +#[cfg(feature = "host")] +use crate::error::NssaCoreError; + pub type ProgramId = [u32; 8]; pub type InstructionData = Vec; pub const DEFAULT_PROGRAM_ID: ProgramId = [0; 8]; @@ -12,12 +15,26 @@ pub struct ProgramInput { pub instruction: T, } -#[derive(Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ProgramOutput { pub pre_states: Vec, pub post_states: Vec, } +#[cfg(feature = "host")] +impl ProgramOutput { + pub fn to_bytes(&self) -> Result, NssaCoreError> { + use risc0_zkvm::serde::to_vec; + + let mut result = Vec::new(); + let b = to_vec(self).map_err(|e| NssaCoreError::DeserializationError(e.to_string()))?; + for word in &b { + result.extend_from_slice(&word.to_le_bytes()); + } + Ok(result) + } +} + pub fn read_nssa_inputs() -> ProgramInput { let pre_states: Vec = env::read(); let words: InstructionData = env::read(); @@ -86,3 +103,60 @@ pub fn validate_execution( true } + +#[cfg(test)] +mod tests { + use risc0_zkvm::Journal; + + use crate::{ + account::{Account, AccountWithMetadata}, + program::ProgramOutput, + }; + + #[test] + fn test_program_output_to_bytes_is_compatible_with_journal_decode() { + let account_pre1 = Account { + program_owner: [1, 2, 3, 4, 5, 6, 7, 8], + balance: 1112223333444455556666, + data: b"test data 1".to_vec(), + nonce: 3344556677889900, + }; + let account_pre2 = Account { + program_owner: [9, 8, 7, 6, 5, 4, 3, 2], + balance: 18446744073709551615, + data: b"test data 2".to_vec(), + nonce: 3344556677889901, + }; + let account_post1 = Account { + program_owner: [1, 2, 3, 4, 5, 6, 7, 8], + balance: 1, + data: b"other test data 1".to_vec(), + nonce: 113, + }; + let account_post2 = Account { + program_owner: [9, 8, 7, 6, 5, 4, 3, 2], + balance: 2, + data: b"other test data 2".to_vec(), + nonce: 112, + }; + + let program_output = ProgramOutput { + pre_states: vec![ + AccountWithMetadata { + account: account_pre1, + is_authorized: true, + }, + AccountWithMetadata { + account: account_pre2, + is_authorized: false, + }, + ], + post_states: vec![account_post1, account_post2], + }; + + let bytes = program_output.to_bytes().unwrap(); + let journal = Journal::new(bytes); + let decoded_program_output: ProgramOutput = journal.decode().unwrap(); + assert_eq!(program_output, decoded_program_output); + } +} diff --git a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs index 6caebee..f165d86 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -11,7 +11,8 @@ fn main() { let PrivacyPreservingCircuitInput { program_output, visibility_mask, - private_account_data, + private_account_nonces, + private_account_keys, private_account_auth, program_id, commitment_set_digest, @@ -36,7 +37,8 @@ fn main() { assert_eq!(visibility_mask.len(), n_accounts); let n_private_accounts = visibility_mask.iter().filter(|&&flag| flag != 0).count(); - assert_eq!(private_account_data.len(), n_private_accounts); + assert_eq!(private_account_nonces.len(), n_private_accounts); + assert_eq!(private_account_keys.len(), n_private_accounts); let n_auth_private_accounts = visibility_mask.iter().filter(|&&flag| flag == 1).count(); assert_eq!(private_account_auth.len(), n_auth_private_accounts); @@ -57,7 +59,8 @@ fn main() { public_pre_states.push(pre_states[i].clone()); public_post_states.push(post_states[i].clone()); } else { - let (new_nonce, Npk, Ipk, esk) = &private_account_data[i]; + let new_nonce = &private_account_nonces[i]; + let (Npk, Ipk, esk) = &private_account_keys[i]; // Verify authentication if visibility_mask[i] == 1 { diff --git a/nssa/src/lib.rs b/nssa/src/lib.rs index 83180e9..2327514 100644 --- a/nssa/src/lib.rs +++ b/nssa/src/lib.rs @@ -1,8 +1,8 @@ mod address; pub mod error; +mod privacy_preserving_transaction; pub mod program; pub mod public_transaction; -mod privacy_preserving_transaction; mod signature; mod state; @@ -12,3 +12,4 @@ pub use signature::PrivateKey; pub use signature::PublicKey; pub use signature::Signature; pub use state::V01State; + diff --git a/nssa/src/privacy_preserving_transaction/encoding.rs b/nssa/src/privacy_preserving_transaction/encoding.rs index 78cb47c..3ff2953 100644 --- a/nssa/src/privacy_preserving_transaction/encoding.rs +++ b/nssa/src/privacy_preserving_transaction/encoding.rs @@ -1,3 +1,5 @@ +// TODO: Consider switching to deriving Borsh + use std::io::{Cursor, Read}; use nssa_core::{ @@ -9,9 +11,9 @@ use crate::{Address, error::NssaError}; use super::message::Message; -const MESSAGE_ENCODING_PREFIX_LEN: usize = 37; +const MESSAGE_ENCODING_PREFIX_LEN: usize = 22; const MESSAGE_ENCODING_PREFIX: &[u8; MESSAGE_ENCODING_PREFIX_LEN] = - b"NSSA/v0.1/TxMessage/PrivacyPreserving"; + b"\x01/NSSA/v0.1/TxMessage/"; impl Message { pub(crate) fn to_bytes(&self) -> Vec { @@ -136,6 +138,3 @@ impl Message { }) } } - -#[cfg(test)] -mod tests {} diff --git a/nssa/src/privacy_preserving_transaction/message.rs b/nssa/src/privacy_preserving_transaction/message.rs index 5250e07..4dddc2d 100644 --- a/nssa/src/privacy_preserving_transaction/message.rs +++ b/nssa/src/privacy_preserving_transaction/message.rs @@ -45,7 +45,7 @@ pub mod tests { use crate::{Address, privacy_preserving_transaction::message::Message}; - fn message_for_tests() -> Message { + pub fn message_for_tests() -> Message { let account1 = Account::default(); let account2 = Account::default(); diff --git a/nssa/src/privacy_preserving_transaction/mod.rs b/nssa/src/privacy_preserving_transaction/mod.rs index 8d77b90..4fed05e 100644 --- a/nssa/src/privacy_preserving_transaction/mod.rs +++ b/nssa/src/privacy_preserving_transaction/mod.rs @@ -1,6 +1,63 @@ -mod transaction; -mod message; -mod witness_set; mod encoding; +mod message; +mod transaction; +mod witness_set; pub use transaction::PrivacyPreservingTransaction; + +pub mod offchain { +// use nssa_core::{ +// account::{Account, AccountWithMetadata, NullifierSecretKey}, program::{InstructionData, ProgramOutput}, PrivacyPreservingCircuitInput +// }; +// +// use crate::{error::NssaError, program::Program}; +// + // pub type Proof = (); +// +// pub fn execute_offchain( +// pre_states: &[AccountWithMetadata], +// instruction_data: &InstructionData, +// private_account_keys: &[(NullifierSecretKey, ] +// visibility_mask: &[u8], +// commitment_set_digest: [u32; 8], +// program: Program, +// ) -> Result<(Proof, Vec), NssaError> { +// // Prove inner program and get post state of the accounts +// let inner_proof = program.execute_and_prove(pre_states, instruction_data)?; +// +// let program_output: ProgramOutput = inner_proof.journal.decode()?; +// +// // Sample fresh random nonces for the outputs of this execution +// let output_nonces: Vec<_> = (0..inputs.len()).map(|_| new_random_nonce()).collect(); +// +// let privacy_preserving_circuit_input = PrivacyPreservingCircuitInput { +// program_output, +// visibility_mask, +// private_account_data: todo!(), +// private_account_auth: todo!(), +// program_id: todo!(), +// commitment_set_digest, +// }; +// // +// // // Prove outer program. +// // let mut env_builder = ExecutorEnv::builder(); +// // env_builder.add_assumption(inner_receipt); +// // env_builder.write(&inner_program_output).unwrap(); +// // env_builder.write(&visibilities).unwrap(); +// // env_builder.write(&output_nonces).unwrap(); +// // env_builder.write(&commitment_tree_root).unwrap(); +// // env_builder.write(&P::PROGRAM_ID).unwrap(); +// // let env = env_builder.build().unwrap(); +// // let prover = default_prover(); +// // let prove_info = prover.prove(env, OUTER_ELF).unwrap(); +// // +// // // Build private accounts. +// // let private_outputs = build_private_outputs_from_inner_results( +// // &inner_program_output, +// // visibilities, +// // &output_nonces, +// // ); +// // +// // Ok((prove_info.receipt, private_outputs)) +// } +} diff --git a/nssa/src/privacy_preserving_transaction/transaction.rs b/nssa/src/privacy_preserving_transaction/transaction.rs index 1b97591..84fea64 100644 --- a/nssa/src/privacy_preserving_transaction/transaction.rs +++ b/nssa/src/privacy_preserving_transaction/transaction.rs @@ -1,9 +1,10 @@ use std::collections::{HashMap, HashSet}; -use nssa_core::{CommitmentSetDigest, EncryptedAccountData}; use nssa_core::account::{Account, AccountWithMetadata}; +use nssa_core::{CommitmentSetDigest, EncryptedAccountData}; use crate::error::NssaError; +use crate::program::Proof; use crate::{Address, V01State}; use super::message::Message; @@ -16,6 +17,13 @@ pub struct PrivacyPreservingTransaction { } 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, @@ -89,7 +97,7 @@ impl PrivacyPreservingTransaction { // 4. Proof verification check_privacy_preserving_circuit_proof_is_valid( - witness_set.proof, + &witness_set.proof, &public_pre_states, &message.public_post_states, &message.encrypted_private_post_states, @@ -130,7 +138,7 @@ impl PrivacyPreservingTransaction { } fn check_privacy_preserving_circuit_proof_is_valid( - proof: (), + proof: &Proof, public_pre_states: &[AccountWithMetadata], public_post_states: &[Account], encrypted_private_post_states: &[EncryptedAccountData], @@ -149,5 +157,5 @@ fn n_unique(data: &[T]) -> usize { #[cfg(test)] mod tests { - + use crate::privacy_preserving_transaction::message::tests::message_for_tests; } diff --git a/nssa/src/privacy_preserving_transaction/witness_set.rs b/nssa/src/privacy_preserving_transaction/witness_set.rs index 285a475..920adb0 100644 --- a/nssa/src/privacy_preserving_transaction/witness_set.rs +++ b/nssa/src/privacy_preserving_transaction/witness_set.rs @@ -1,6 +1,7 @@ -use crate::{PrivateKey, PublicKey, Signature, privacy_preserving_transaction::message::Message}; - -type Proof = (); +use crate::{ + PrivateKey, PublicKey, Signature, privacy_preserving_transaction::message::Message, + program::Proof, +}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct WitnessSet { @@ -10,18 +11,30 @@ pub struct WitnessSet { impl WitnessSet { pub fn for_message(message: &Message, proof: Proof, private_keys: &[&PrivateKey]) -> Self { - todo!() + 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 - todo!() + 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)] { diff --git a/nssa/src/program.rs b/nssa/src/program.rs index b104eca..0bd6dcb 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -1,15 +1,19 @@ +use borsh::{BorshDeserialize, BorshSerialize}; use nssa_core::{ account::{Account, AccountWithMetadata}, program::{DEFAULT_PROGRAM_ID, InstructionData, ProgramId, ProgramOutput}, }; use program_methods::{AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID}; use risc0_zkvm::{ - ExecutorEnv, ExecutorEnvBuilder, Receipt, default_executor, default_prover, serde::to_vec, + ExecutorEnv, ExecutorEnvBuilder, Journal, Receipt, default_executor, default_prover, + serde::to_vec, }; use serde::Serialize; use crate::error::NssaError; +pub type Proof = Vec; + #[derive(Debug, PartialEq, Eq)] pub struct Program { id: ProgramId, @@ -56,11 +60,11 @@ impl Program { /// Executes and proves the program `P`. /// Returns the proof - fn execute_and_prove( + pub(crate) fn execute_and_prove( &self, pre_states: &[AccountWithMetadata], instruction_data: &InstructionData, - ) -> Result { + ) -> Result<(Proof, ProgramOutput), NssaError> { // Write inputs to the program let mut env_builder = ExecutorEnv::builder(); Self::write_inputs(pre_states, instruction_data, &mut env_builder)?; @@ -71,7 +75,9 @@ impl Program { let prove_info = prover .prove(env, self.elf) .map_err(|e| NssaError::ProgramProveFailed(e.to_string()))?; - Ok(prove_info.receipt) + let proof = borsh::to_vec(&prove_info.receipt.inner).unwrap(); + let program_output = prove_info.receipt.journal.decode().unwrap(); + Ok((proof, program_output)) } /// Writes inputs to `env_builder` in the order expected by the programs @@ -101,6 +107,7 @@ mod tests { account::{Account, AccountWithMetadata}, program::ProgramOutput, }; + use risc0_zkvm::{InnerReceipt, Receipt, serde::to_vec}; use crate::program::Program; @@ -247,15 +254,20 @@ mod tests { ..Account::default() }; - let receipt = program + let (proof, program_output) = program .execute_and_prove(&[sender, recipient], &instruction_data) .unwrap(); - let ProgramOutput { post_states, .. } = receipt.journal.decode().unwrap(); + + let ProgramOutput { post_states, .. } = program_output.clone(); let [sender_post, recipient_post] = post_states.try_into().unwrap(); - let output = assert_eq!(sender_post, expected_sender_post); - + assert_eq!(sender_post, expected_sender_post); assert_eq!(recipient_post, expected_recipient_post); - assert!(receipt.verify(program.id()).is_ok()); + + let journal = program_output.to_bytes().unwrap(); + let inner: InnerReceipt = borsh::from_slice(&proof).unwrap(); + let receipt = Receipt::new(inner, journal); + + receipt.verify(program.id()).unwrap(); } } diff --git a/nssa/src/public_transaction/encoding.rs b/nssa/src/public_transaction/encoding.rs index 0e3c8ba..70b5288 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,9 +10,9 @@ use crate::{ public_transaction::{Message, WitnessSet}, }; -const MESSAGE_ENCODING_PREFIX_LEN: usize = 37; +const MESSAGE_ENCODING_PREFIX_LEN: usize = 22; const MESSAGE_ENCODING_PREFIX: &[u8; MESSAGE_ENCODING_PREFIX_LEN] = - b"NSSA/v0.1/TxMessage/Public\0\0\0\0\0\0\0\0\0\0\0"; + b"\x00/NSSA/v0.1/TxMessage/"; impl Message { /// Serializes a `Message` into bytes in the following layout: