diff --git a/.gitignore b/.gitignore index f2f9e58..d97316e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ target -Cargo.lock \ No newline at end of file +Cargo.lock +risc0-selective-privacy-poc/target +risc0-selective-privacy-poc/Cargo.lock +risc0-selective-privacy-poc/methods/guest/Cargo.lock \ No newline at end of file diff --git a/risc0-selective-privacy-poc/Cargo.toml b/risc0-selective-privacy-poc/Cargo.toml new file mode 100644 index 0000000..06b2f83 --- /dev/null +++ b/risc0-selective-privacy-poc/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "nssa" +version = "0.12.0" +edition = "2021" + +[dependencies] +risc0-zkvm = "2.2" +core = { path = "core" } +program-methods = { path = "program_methods" } +serde = "1.0" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +rand = "0.8" +sparse-merkle-tree = { path = "./sparse_merkle_tree/" } + +[features] +cuda = ["risc0-zkvm/cuda"] +default = [] +prove = ["risc0-zkvm/prove"] + diff --git a/risc0-selective-privacy-poc/core/Cargo.toml b/risc0-selective-privacy-poc/core/Cargo.toml new file mode 100644 index 0000000..4e57901 --- /dev/null +++ b/risc0-selective-privacy-poc/core/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "core" +version = "0.12.0" +edition = "2021" + +[dependencies] +risc0-zkvm = "2.0.2" +serde = { version = "1.0", default-features = false } + diff --git a/risc0-selective-privacy-poc/core/src/account.rs b/risc0-selective-privacy-poc/core/src/account.rs new file mode 100644 index 0000000..088b7ee --- /dev/null +++ b/risc0-selective-privacy-poc/core/src/account.rs @@ -0,0 +1,42 @@ +use crate::{ + hash, + types::{Address, Commitment, Key, Nonce, ProgramId}, +}; +use risc0_zkvm::serde::to_vec; +use serde::{Deserialize, Serialize}; + +/// Account to be used both in public and private contexts +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct Account { + pub program_owner: Option, + pub address: Address, + pub balance: u128, + pub nonce: Nonce, +} + +impl Account { + pub fn new(address: Address, balance: u128) -> Self { + Self { + program_owner: None, + address, + balance, + nonce: [0; 8], + } + } + + /// Creates a new account with address = hash(private_key) and balance = 0 + pub fn new_from_private_key(private_key: Key) -> Self { + let address = Self::address_for_key(&private_key); + Self::new(address, 0) + } + + /// Computes the address corresponding to the given private key + pub fn address_for_key(private_key: &Key) -> Address { + hash(private_key) + } + + /// Returns (first 8 bytes of) SHA256(Account) + pub fn commitment(&self) -> Commitment { + hash(&to_vec(&self).unwrap())[0] + } +} diff --git a/risc0-selective-privacy-poc/core/src/lib.rs b/risc0-selective-privacy-poc/core/src/lib.rs new file mode 100644 index 0000000..bde57f9 --- /dev/null +++ b/risc0-selective-privacy-poc/core/src/lib.rs @@ -0,0 +1,126 @@ +pub mod account; +pub mod types; +pub mod visibility; + +use std::collections::HashSet; + +use crate::{ + account::Account, + types::{AuthenticationPath, Commitment, Key, Nullifier, ProgramId}, +}; +use risc0_zkvm::sha::{Impl, Sha256}; + +pub fn hash(bytes: &[u32]) -> [u32; 8] { + Impl::hash_words(bytes).as_words().try_into().unwrap() +} + +pub fn is_in_tree(commitment: Commitment, path: &AuthenticationPath, root: [u32; 8]) -> bool { + const HASH_ONE: [u32; 8] = [ + 789771595, 3310634292, 3140410939, 3820475020, 3591004369, 2777006897, 1021496535, 2588247415, + ]; + + let mut hash = HASH_ONE; + let mut current_index = commitment; + for path_value in path.iter() { + if current_index & 1 == 0 { + hash = hash_two(&hash, path_value); + } else { + hash = hash_two(path_value, &hash); + } + current_index >>= 1; + } + root == hash +} + +/// Returns Hash(Commitment || private_key) +pub fn compute_nullifier(commitment: &Commitment, private_key: &Key) -> Nullifier { + let mut bytes_to_hash = [0; 9]; // <- 1 word for the commitment, 8 words for the private key + bytes_to_hash[..1].copy_from_slice(&[*commitment]); + bytes_to_hash[1..].copy_from_slice(private_key); + hash(&bytes_to_hash) +} + +fn hash_two(left: &[u32; 8], right: &[u32; 8]) -> [u32; 8] { + let mut bytes_to_hash = [0; 16]; + bytes_to_hash[..8].copy_from_slice(left); + bytes_to_hash[8..].copy_from_slice(right); + hash(&bytes_to_hash) +} + +pub fn bytes_to_words(bytes: &[u8; 32]) -> [u32; 8] { + let mut words = [0; 8]; + for (i, chunk) in bytes.chunks(4).enumerate() { + words[i] = u32::from_le_bytes(chunk.try_into().unwrap()); + } + words +} + +/// Ensures that account transitions follow the rules of a well-behaved program. +/// +/// A well-behaved program is one that: +/// - does not change account addresses +/// - does not change account nonces +/// - does not change the `program_owner` field +/// - only reduces the balance of accounts it owns and for which authentication was provided +/// (**NOTE**: this authentication check is **not** included in this proof of concept) +/// - preserves the total token supply across all accounts +/// +/// This function does **not** check that the output accounts are the result of correctly +/// executing the program. That must be checked separately, either by re-executing +/// the program with the inputs or by verifying a proof of correct execution. +pub fn check_well_behaved_account_transition( + input_accounts: &[Account], + output_accounts: &[Account], + program_id: ProgramId, +) -> bool { + // Fail if the number of input and output accounts differ + if input_accounts.len() != output_accounts.len() { + return false; + } + + for (account_pre, account_post) in input_accounts.iter().zip(output_accounts) { + // Fail if the program modified the addresses of the input accounts + if account_pre.address != account_post.address { + return false; + } + + // Fail if the program modified the nonces of the input accounts + if account_pre.nonce != account_post.nonce { + return false; + } + + // Fail if the program modified the program owner + if account_pre.program_owner != account_post.program_owner { + return false; + } + + // Fail if the program subtracted balance from an account it doesn't own. + // (This check always passes if `program_owner` is `None`) + if account_pre.balance > account_post.balance && account_pre.program_owner.unwrap_or(program_id) != program_id { + return false; + } + } + + // Fail if the execution didn't preserve the total supply. + let total_balance_pre: u128 = input_accounts.iter().map(|account| account.balance).sum(); + let total_balance_post: u128 = output_accounts.iter().map(|account| account.balance).sum(); + if total_balance_pre != total_balance_post { + return false; + } + // The previous check is only meaningful if the accounts involved in the sum are all different + // otherwise the same balance is counted more than once in the total balance check. Therefore, we need to check + // that the set of input accounts doesn't have repeated pairs of (address, nonce). + // Note: Checking only that addresses are unique would be too restrictive, since different private accounts can have + // the same address (but different nonces). + if input_accounts + .iter() + .map(|account| (account.address, account.nonce)) + .collect::>() + .len() + != input_accounts.len() + { + return false; + } + + true +} diff --git a/risc0-selective-privacy-poc/core/src/types.rs b/risc0-selective-privacy-poc/core/src/types.rs new file mode 100644 index 0000000..a2bda29 --- /dev/null +++ b/risc0-selective-privacy-poc/core/src/types.rs @@ -0,0 +1,27 @@ +use serde::{Deserialize, Serialize}; + +use crate::account::Account; + +/// For this POC we consider 32-bit commitments +pub type Commitment = u32; +pub type Nullifier = [u32; 8]; +pub type Address = [u32; 8]; +pub type Nonce = [u32; 8]; +pub type Key = [u32; 8]; +pub type AuthenticationPath = [[u32; 8]; 32]; +pub type ProgramId = [u32; 8]; + +#[derive(Serialize, Deserialize)] +pub struct ProgramOutput { + pub accounts_pre: Vec, + pub accounts_post: Vec, +} + +#[derive(Serialize, Deserialize)] +pub struct PrivacyExecutionOutput { + pub public_accounts_pre: Vec, + pub public_accounts_post: Vec, + pub private_output_commitments: Vec, + pub nullifiers: Vec, + pub commitment_tree_root: [u32; 8], +} diff --git a/risc0-selective-privacy-poc/core/src/visibility.rs b/risc0-selective-privacy-poc/core/src/visibility.rs new file mode 100644 index 0000000..79c83e3 --- /dev/null +++ b/risc0-selective-privacy-poc/core/src/visibility.rs @@ -0,0 +1,10 @@ +use crate::types::{AuthenticationPath, Key}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub enum AccountVisibility { + // A public account + Public, + // A private account + Private(Option<(Key, AuthenticationPath)>), +} diff --git a/risc0-selective-privacy-poc/examples/happy_path.rs b/risc0-selective-privacy-poc/examples/happy_path.rs new file mode 100644 index 0000000..f6b3fbc --- /dev/null +++ b/risc0-selective-privacy-poc/examples/happy_path.rs @@ -0,0 +1,88 @@ +use core::{bytes_to_words, types::Address, visibility::AccountVisibility}; + +use nssa::program::PinataProgram; + +use crate::mocked_components::sequencer::{print_accounts, MockedSequencer, PINATA_ADDRESS}; +use crate::mocked_components::{client::MockedClient, USER_CLIENTS}; + +mod mocked_components; + +fn main() { + let mut sequencer = MockedSequencer::new(); + let addresses: [Address; 3] = USER_CLIENTS.map(|client| client.user_address()); + println!("📝 Initial balances"); + print_accounts(&sequencer, &[]); + + // A public execution of the Transfer Program + // User1 sends 51 tokens to the piñata account + USER_CLIENTS[1] + .transfer_public(&PINATA_ADDRESS, 51, &mut sequencer) + .unwrap(); + println!("📝 Balances after transfer"); + print_accounts(&sequencer, &[]); + + // A shielded execution of the Transfer Program + // User0 shields 15 tokens to a new private account of User1 + let private_account_user_1 = USER_CLIENTS[0] + .transfer_shielded(&addresses[1], 15, &mut sequencer) + .unwrap(); + println!("📝 Balances after shielded execution"); + print_accounts(&sequencer, &[&private_account_user_1]); + + // A private execution of the Transfer Program + // User1 uses it's private account to send 8 tokens to a new private account of User2 + let [private_account_user_1, private_account_user_2] = USER_CLIENTS[1] + .transfer_private(private_account_user_1, &addresses[2], 8, &mut sequencer) + .unwrap(); + println!("📝 Balances after private execution"); + print_accounts(&sequencer, &[&private_account_user_1, &private_account_user_2]); + + // A deshielded execution of the Transfer Program + // User2 deshields 1 token to the public account of User0 + let private_account_user_2 = USER_CLIENTS[2] + .transfer_deshielded(private_account_user_2, &addresses[0], 1, &mut sequencer) + .unwrap(); + println!("📝 Balances after deshielded execution"); + print_accounts(&sequencer, &[&private_account_user_1, &private_account_user_2]); + + // A public execution of the Piñata program + // User2 claims the prize of the Piñata program to its public account + let preimage = bytes_to_words(b"NSSA Selective privacy is great!").to_vec(); + sequencer + .process_public_execution::(&[PINATA_ADDRESS, addresses[2]], preimage) + .unwrap(); + println!("📝 Balances after public piñata execution"); + print_accounts(&sequencer, &[&private_account_user_1, &private_account_user_2]); + + // A shielded execution of the Piñata program + // User1 claims the prize of the Piñata program to a new self-owned private account + let another_private_account_user_1 = { + // All of this is executed locally by the User1 + let pinata_account = sequencer.get_account(&PINATA_ADDRESS).unwrap(); + let receiver_account = MockedClient::fresh_account_for_mint(USER_CLIENTS[1].user_address()); + let visibilities = [AccountVisibility::Public, AccountVisibility::Private(None)]; + let preimage = bytes_to_words(b"NSSA Selective privacy is great!").to_vec(); + + let private_outputs = MockedClient::prove_and_send_to_sequencer::( + &[pinata_account, receiver_account], + preimage, + &visibilities, + sequencer.get_commitment_tree_root(), + &mut sequencer, + ) + .unwrap(); + let [private_account_user_1] = private_outputs.try_into().unwrap(); + private_account_user_1 + }; + println!("📝 Balances after private piñata execution"); + print_accounts( + &sequencer, + &[ + &private_account_user_1, + &private_account_user_2, + &another_private_account_user_1, + ], + ); + + println!("Ok!"); +} diff --git a/risc0-selective-privacy-poc/examples/mocked_components/client/mod.rs b/risc0-selective-privacy-poc/examples/mocked_components/client/mod.rs new file mode 100644 index 0000000..563fc21 --- /dev/null +++ b/risc0-selective-privacy-poc/examples/mocked_components/client/mod.rs @@ -0,0 +1,54 @@ +use crate::mocked_components::sequencer::MockedSequencer; +use core::{ + account::Account, + types::{Address, Key}, + visibility::AccountVisibility, +}; + +use super::sequencer::error::Error; + +pub mod transfer_deshielded; +pub mod transfer_private; +pub mod transfer_public; +pub mod transfer_shielded; + +/// A client that creates and submits transfer transactions +pub struct MockedClient { + user_private_key: Key, +} + +impl MockedClient { + pub const fn new(user_private_key: Key) -> Self { + Self { user_private_key } + } + + pub fn user_address(&self) -> Address { + Account::address_for_key(&self.user_private_key) + } + + /// Runs the outer program and submits the proof to the sequencer. + /// Returns the output private accounts of the execution. + pub fn prove_and_send_to_sequencer( + input_accounts: &[Account], + instruction_data: P::InstructionData, + visibilities: &[AccountVisibility], + commitment_tree_root: [u32; 8], + sequencer: &mut MockedSequencer, + ) -> Result, Error> { + // Execute and generate proof of the outer program + let (receipt, private_outputs) = + nssa::execute_offchain::

(input_accounts, instruction_data, visibilities, commitment_tree_root) + .map_err(|_| Error::BadInput)?; + + // Send proof to the sequencer + sequencer.process_privacy_execution(receipt)?; + + // Return private outputs + Ok(private_outputs) + } + + /// Returns a new account used in privacy executions that mint private accounts + pub fn fresh_account_for_mint(address: Address) -> Account { + Account::new(address, 0) + } +} diff --git a/risc0-selective-privacy-poc/examples/mocked_components/client/transfer_deshielded.rs b/risc0-selective-privacy-poc/examples/mocked_components/client/transfer_deshielded.rs new file mode 100644 index 0000000..b86ba9d --- /dev/null +++ b/risc0-selective-privacy-poc/examples/mocked_components/client/transfer_deshielded.rs @@ -0,0 +1,47 @@ +use super::{MockedClient, MockedSequencer}; +use crate::mocked_components::sequencer::error::Error; +use core::account::Account; +use core::types::Address; +use core::visibility::AccountVisibility; +use nssa::program::TransferProgram; + +impl MockedClient { + /// A deshielded transaction of the Transfer program. + /// All of this is executed locally by the sender + pub fn transfer_deshielded( + &self, + from_account: Account, + to_address: &Address, + balance_to_move: u128, + sequencer: &mut MockedSequencer, + ) -> Result { + // Fetch commitment tree root from the sequencer + let commitment_tree_root = sequencer.get_commitment_tree_root(); + // Compute authenticaton path for the input private account + let sender_commitment_auth_path = sequencer.get_authentication_path_for(&from_account.commitment()); + + // Fetch public account to deshield to + let to_account = sequencer.get_account(to_address).unwrap(); + + // Set account visibilities + // First entry is the private sender. Second entry is the public receiver + let visibilities = vec![ + AccountVisibility::Private(Some((self.user_private_key, sender_commitment_auth_path))), + AccountVisibility::Public, + ]; + + // Execute privately (off-chain) and submit it to the sequencer + let private_outputs = Self::prove_and_send_to_sequencer::( + &[from_account, to_account], + balance_to_move, + &visibilities, + commitment_tree_root, + sequencer, + )?; + + // There's only one private output account corresponding to the new private account of + // the sender, with the remaining balance. + let [sender_private_account] = private_outputs.try_into().unwrap(); + Ok(sender_private_account) + } +} diff --git a/risc0-selective-privacy-poc/examples/mocked_components/client/transfer_private.rs b/risc0-selective-privacy-poc/examples/mocked_components/client/transfer_private.rs new file mode 100644 index 0000000..a1a8d31 --- /dev/null +++ b/risc0-selective-privacy-poc/examples/mocked_components/client/transfer_private.rs @@ -0,0 +1,47 @@ +use super::{MockedClient, MockedSequencer}; +use crate::mocked_components::sequencer::error::Error; +use core::account::Account; +use core::types::Address; +use core::visibility::AccountVisibility; +use nssa::program::TransferProgram; + +impl MockedClient { + /// A private execution of the Transfer program + // All of this is executed locally by the sender + pub fn transfer_private( + &self, + owned_private_account: Account, + to_address: &Address, + balance_to_move: u128, + sequencer: &mut MockedSequencer, + ) -> Result<[Account; 2], Error> { + // Fetch commitment tree root from the sequencer + let commitment_tree_root = sequencer.get_commitment_tree_root(); + // Compute authenticaton path for the input private account + let sender_commitment_auth_path = sequencer.get_authentication_path_for(&owned_private_account.commitment()); + + // Create a new default private account for the recipient + let receiver_account = Self::fresh_account_for_mint(*to_address); + + // Set visibilities. Both private accounts. + let visibilities = vec![ + AccountVisibility::Private(Some((self.user_private_key, sender_commitment_auth_path))), + AccountVisibility::Private(None), + ]; + + // Execute privately (off-chain) and submit it to the sequencer + let private_outputs = Self::prove_and_send_to_sequencer::( + &[owned_private_account, receiver_account], + balance_to_move, + &visibilities, + commitment_tree_root, + sequencer, + )?; + + // There are two output private accounts of this execution. + // The first corresponds to the sender, with the remaining balance. + // The second corresponds to the newly minted private account for the recipient. + let [sender_private_account, receiver_private_account] = private_outputs.try_into().unwrap(); + Ok([sender_private_account, receiver_private_account]) + } +} diff --git a/risc0-selective-privacy-poc/examples/mocked_components/client/transfer_public.rs b/risc0-selective-privacy-poc/examples/mocked_components/client/transfer_public.rs new file mode 100644 index 0000000..cdbb775 --- /dev/null +++ b/risc0-selective-privacy-poc/examples/mocked_components/client/transfer_public.rs @@ -0,0 +1,22 @@ +use core::types::Address; + +use nssa::program::TransferProgram; + +use crate::mocked_components::{ + client::MockedClient, + sequencer::{error::Error, MockedSequencer}, +}; + +impl MockedClient { + pub fn transfer_public( + &self, + to_address: &Address, + amount_to_transfer: u128, + sequencer: &mut MockedSequencer, + ) -> Result<(), Error> { + // Submit a public (on-chain) execution of the Transfer program to the sequencer + sequencer + .process_public_execution::(&[self.user_address(), *to_address], amount_to_transfer) + .map_err(|_| Error::BadInput) + } +} diff --git a/risc0-selective-privacy-poc/examples/mocked_components/client/transfer_shielded.rs b/risc0-selective-privacy-poc/examples/mocked_components/client/transfer_shielded.rs new file mode 100644 index 0000000..58a4c06 --- /dev/null +++ b/risc0-selective-privacy-poc/examples/mocked_components/client/transfer_shielded.rs @@ -0,0 +1,45 @@ +use super::{MockedClient, MockedSequencer}; +use crate::mocked_components::sequencer::error::Error; +use core::account::Account; +use core::types::Address; +use core::visibility::AccountVisibility; +use nssa::program::TransferProgram; + +impl MockedClient { + /// A shielded execution of the Transfer program + // All of this is executed locally by the sender + pub fn transfer_shielded( + &self, + to_address: &Address, + balance_to_move: u128, + sequencer: &mut MockedSequencer, + ) -> Result { + // Fetch commitment tree root from the sequencer + let commitment_tree_root = sequencer.get_commitment_tree_root(); + + // Fetch sender account from the sequencer + let from_account = sequencer.get_account(&self.user_address()).ok_or(Error::NotFound)?; + + // Create a new default private account for the receiver + let to_account = Self::fresh_account_for_mint(*to_address); + + // Set account visibilities + // First is the public account of the sender. Second is the private account minted in this + // execution + let visibilities = [AccountVisibility::Public, AccountVisibility::Private(None)]; + + // Execute privately (off-chain) and submit it to the sequencer + let private_outputs = Self::prove_and_send_to_sequencer::( + &[from_account, to_account], + balance_to_move, + &visibilities, + commitment_tree_root, + sequencer, + )?; + + // There is only one private account as the output of this execution. It corresponds to the + // receiver. + let [receiver_private_account] = private_outputs.try_into().unwrap(); + Ok(receiver_private_account) + } +} diff --git a/risc0-selective-privacy-poc/examples/mocked_components/mod.rs b/risc0-selective-privacy-poc/examples/mocked_components/mod.rs new file mode 100644 index 0000000..7ba4703 --- /dev/null +++ b/risc0-selective-privacy-poc/examples/mocked_components/mod.rs @@ -0,0 +1,11 @@ +use crate::mocked_components::client::MockedClient; + +pub mod client; +pub mod sequencer; + +/// Default users for examples +pub const USER_CLIENTS: [MockedClient; 3] = [ + MockedClient::new([1; 8]), + MockedClient::new([2; 8]), + MockedClient::new([3; 8]), +]; diff --git a/risc0-selective-privacy-poc/examples/mocked_components/sequencer/error.rs b/risc0-selective-privacy-poc/examples/mocked_components/sequencer/error.rs new file mode 100644 index 0000000..f3b3fbc --- /dev/null +++ b/risc0-selective-privacy-poc/examples/mocked_components/sequencer/error.rs @@ -0,0 +1,16 @@ +#[derive(Debug)] +pub enum Error { + NotFound, + BadInput, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::NotFound => write!(f, "Not found"), + Error::BadInput => write!(f, "Bad input"), + } + } +} + +impl std::error::Error for Error {} diff --git a/risc0-selective-privacy-poc/examples/mocked_components/sequencer/mod.rs b/risc0-selective-privacy-poc/examples/mocked_components/sequencer/mod.rs new file mode 100644 index 0000000..e66bf83 --- /dev/null +++ b/risc0-selective-privacy-poc/examples/mocked_components/sequencer/mod.rs @@ -0,0 +1,130 @@ +use core::{ + account::Account, + bytes_to_words, + types::{Address, AuthenticationPath, Commitment, Nullifier}, +}; +use std::collections::{BTreeMap, HashSet}; + +use nssa::{program::PinataProgram, Program}; +use sparse_merkle_tree::SparseMerkleTree; + +use crate::mocked_components::USER_CLIENTS; + +pub mod error; +pub mod process_privacy_execution; +pub mod process_public_execution; + +pub struct MockedSequencer { + accounts: BTreeMap, + commitment_tree: SparseMerkleTree, + nullifier_set: HashSet, +} + +/// The initial balance of the genesis accounts +const INITIAL_BALANCE: u128 = 150; +/// The address of the piñata program account +pub const PINATA_ADDRESS: Address = [0xcafe; 8]; + +impl MockedSequencer { + pub fn new() -> Self { + let mut accounts: BTreeMap = USER_CLIENTS + .iter() + .map(|client| client.user_address()) + .map(|address| Account::new(address, INITIAL_BALANCE)) + .map(|account| (account.address, account)) + .collect(); + + let pinata_account = { + let mut this = Account::new(PINATA_ADDRESS, INITIAL_BALANCE); + // Set the owner of the Pinata account so that only the Pinata program + // can reduce its balance. + this.program_owner = Some(PinataProgram::PROGRAM_ID); + this + }; + + accounts.insert(pinata_account.address, pinata_account); + + let commitment_tree = SparseMerkleTree::new_empty(); + let nullifier_set = HashSet::new(); + Self { + accounts, + commitment_tree, + nullifier_set, + } + } + + /// Returns the current state of the account for the given address + pub fn get_account(&self, address: &Address) -> Option { + self.accounts.get(address).cloned() + } + + /// Returns the root of the commitment tree + pub fn get_commitment_tree_root(&self) -> [u32; 8] { + bytes_to_words(&self.commitment_tree.root()) + } + + /// Computes the authentication path for the given commitment + pub fn get_authentication_path_for(&self, commitment: &Commitment) -> AuthenticationPath { + self.commitment_tree + .get_authentication_path_for_value(*commitment) + .iter() + .map(bytes_to_words) + .collect::>() + .try_into() + .unwrap() + } +} + +/// Pretty prints the chain's state +pub fn print_accounts(sequencer: &MockedSequencer, private_accounts: &[&Account]) { + println!("\n====================== ACCOUNT SNAPSHOT ======================\n"); + + println!(">> Public Accounts:"); + println!("{:<20} | {:>10} |", "Address (first u32)", "Balance"); + println!("{:-<20}-+-{:-<10}", "", ""); + + for account in sequencer.accounts.values() { + println!("0x{:<20x} | {:>10} |", account.address[0], account.balance); + } + + println!("{:-<20}-+-{:-<10}\n", "", ""); + + println!(">> Commitments:"); + println!("{:-<20}", ""); + + for commitment in sequencer.commitment_tree.values().iter() { + println!("{commitment:<20x}"); + } + + println!("{:-<20}\n", ""); + + let formatted: Vec = sequencer + .nullifier_set + .iter() + .map(|nullifier| format!("0x{:x}", nullifier[0])) + .collect(); + + println!(">> Nullifiers (first u32):"); + println!("{:-<20}", ""); + + for entry in formatted { + println!("{entry:<20}"); + } + + println!("{:-<20}\n", ""); + + println!(">> Private Accounts:"); + println!("{:<20} | {:>10} | {:>10}", "Address (first u32)", "Nonce", "Balance"); + println!("{:-<20}-+-{:-<10}-+-{:-<10}", "", "", ""); + + for account in private_accounts.iter() { + println!( + "{:<20x} | {:>10x}| {:>10} | ", + account.address[0], account.nonce[0], account.balance, + ); + } + + println!("{:-<20}-+-{:-<10}-+-{:-<10}", "", "", ""); + + println!("\n=============================================================\n"); +} diff --git a/risc0-selective-privacy-poc/examples/mocked_components/sequencer/process_privacy_execution.rs b/risc0-selective-privacy-poc/examples/mocked_components/sequencer/process_privacy_execution.rs new file mode 100644 index 0000000..0b278cd --- /dev/null +++ b/risc0-selective-privacy-poc/examples/mocked_components/sequencer/process_privacy_execution.rs @@ -0,0 +1,74 @@ +use core::types::PrivacyExecutionOutput; + +use risc0_zkvm::Receipt; + +use super::error::Error; +use super::MockedSequencer; + +impl MockedSequencer { + /// Processes a privacy execution request. + /// Verifies the proof of the privacy execution and updates the state of the chain. + pub fn process_privacy_execution(&mut self, receipt: Receipt) -> Result<(), Error> { + // Parse the output of the "outer" program + let output: PrivacyExecutionOutput = receipt.journal.decode().unwrap(); + + // Reject if the states of the public input accounts used in the inner execution do not + // coincide with the on-chain state. + for account in output.public_accounts_pre.iter() { + let current_account = self.get_account(&account.address).ok_or(Error::NotFound)?; + if ¤t_account != account { + return Err(Error::BadInput); + } + } + + // Reject in case the root used in the privacy execution is not the current root. + if output.commitment_tree_root != self.get_commitment_tree_root() { + return Err(Error::BadInput); + } + + // Reject if the nullifiers of this privacy execution have already been published. + if output + .nullifiers + .iter() + .any(|nullifier| self.nullifier_set.contains(nullifier)) + { + return Err(Error::BadInput); + } + + // Reject if the commitments have already been seen. + if output + .private_output_commitments + .iter() + .any(|commitment| self.commitment_tree.values().contains(commitment)) + { + return Err(Error::BadInput); + } + + // Verify the proof of the privacy execution. + // This includes a proof of the following statements + // - Public inputs, public outputs, commitments and nullifiers are consistent with the + // execution of some program. + // - The given nullifiers correctly correspond to commitments that currently belong to + // the commitment tree. + // - The given commitments are correctly computed from valid accounts. + // - The chain invariants are preserved + nssa::verify_privacy_execution(receipt).map_err(|_| Error::BadInput)?; + + // At this point the privacy execution is considered valid. + // + // Update the state of the public accounts with the post-state of this privacy execution + output.public_accounts_post.into_iter().for_each(|account_post_state| { + self.accounts.insert(account_post_state.address, account_post_state); + }); + + // Add all nullifiers to the nullifier set. + self.nullifier_set.extend(output.nullifiers); + + // Add commitments to the commitment tree. + for commitment in output.private_output_commitments.iter() { + self.commitment_tree.add_value(*commitment); + } + + Ok(()) + } +} diff --git a/risc0-selective-privacy-poc/examples/mocked_components/sequencer/process_public_execution.rs b/risc0-selective-privacy-poc/examples/mocked_components/sequencer/process_public_execution.rs new file mode 100644 index 0000000..fcf7562 --- /dev/null +++ b/risc0-selective-privacy-poc/examples/mocked_components/sequencer/process_public_execution.rs @@ -0,0 +1,36 @@ +use core::{account::Account, check_well_behaved_account_transition, types::Address}; + +use crate::mocked_components::sequencer::error::Error; + +use super::MockedSequencer; + +impl MockedSequencer { + /// Processes a public execution request of the program `P`. + pub fn process_public_execution( + &mut self, + input_account_addresses: &[Address], + instruction_data: P::InstructionData, + ) -> Result<(), Error> { + // Fetch the current state of the input accounts. + let input_accounts: Vec = input_account_addresses + .iter() + .map(|address| self.get_account(address).ok_or(Error::NotFound)) + .collect::>()?; + + // Execute the program + let program_output = + nssa::execute_onchain::

(&input_accounts, instruction_data).map_err(|_| Error::BadInput)?; + + // Assert accounts pre- and post-states preserve chains invariants + if !check_well_behaved_account_transition(&input_accounts, &program_output.accounts_post, P::PROGRAM_ID) { + return Err(Error::BadInput); + } + + // Update the accounts states + program_output.accounts_post.into_iter().for_each(|account_post_state| { + self.accounts.insert(account_post_state.address, account_post_state); + }); + + Ok(()) + } +} diff --git a/risc0-selective-privacy-poc/examples/private_execution.rs b/risc0-selective-privacy-poc/examples/private_execution.rs new file mode 100644 index 0000000..af80da9 --- /dev/null +++ b/risc0-selective-privacy-poc/examples/private_execution.rs @@ -0,0 +1,70 @@ +use core::{ + account::Account, + bytes_to_words, + types::{Address, AuthenticationPath}, + visibility::AccountVisibility, +}; +use nssa::program::TransferMultipleProgram; +use sparse_merkle_tree::SparseMerkleTree; + +/// A private execution of the TransferMultiple function. +/// This actually "burns" a sender private account and "mints" two new private accounts: +/// one for the recipient with the transferred balance, and another owned by the sender with the remaining balance. +fn main() { + // Setup commitment tree, simulating a current chain state that has a private account already + // committed to the commitment tree. + let sender_private_key = [1, 2, 3, 4, 4, 3, 2, 1]; + let sender = { + // Creating this here but it is supposed to be already in the private possesion of the user + let mut account = Account::new_from_private_key(sender_private_key); + account.balance = 150; + account + }; + let commitment_tree = SparseMerkleTree::new([sender.commitment()].into_iter().collect()); + + // Get the root of the commitment tree and the authentication path of the commitment of the private account. + let root = bytes_to_words(&commitment_tree.root()); + let auth_path: Vec<[u32; 8]> = commitment_tree + .get_authentication_path_for_value(sender.commitment()) + .iter() + .map(bytes_to_words) + .collect(); + let auth_path: AuthenticationPath = auth_path.try_into().unwrap(); + + // These are the new private account being minted by this private execution. + // (the `receiver_address` would be in UTXO's terminology) + let receiver_address_1 = [99; 8]; + let receiver_1 = new_default_account(receiver_address_1); + let receiver_address_2 = [100; 8]; + let receiver_2 = new_default_account(receiver_address_2); + + // Setup input account visibilites. All accounts are private for this execution. + let visibilities = vec![ + AccountVisibility::Private(Some((sender_private_key, auth_path))), + AccountVisibility::Private(None), + AccountVisibility::Private(None), + ]; + + // Set the balances to be sent to the two receiver addresses. + // This means, the execution will remove 70 tokens from the sender + // and send 30 to the first receiver and 40 to the second. + let balance_to_move = vec![30, 40]; + + // Execute and prove the outer program for the TransferMultipleProgram. + // This is executed off-chain by the sender. + let (receipt, _) = nssa::execute_offchain::( + &[sender, receiver_1, receiver_2], + balance_to_move, + &visibilities, + root, + ) + .unwrap(); + + // Verify the proof + assert!(nssa::verify_privacy_execution(receipt).is_ok()); + println!("OK!"); +} + +fn new_default_account(address: Address) -> Account { + Account::new(address, 0) +} diff --git a/risc0-selective-privacy-poc/examples/public_execution.rs b/risc0-selective-privacy-poc/examples/public_execution.rs new file mode 100644 index 0000000..f1f59e6 --- /dev/null +++ b/risc0-selective-privacy-poc/examples/public_execution.rs @@ -0,0 +1,24 @@ +use core::account::Account; + +use nssa::program::TransferMultipleProgram; + +/// A public execution of the TransferMultipleProgram. +/// This would be executed by the runtime after checking that +/// the initiating transaction includes the sender's signature. +pub fn main() { + // Account fetched from the chain state with 150 in its balance. + let sender = Account::new([5; 8], 150); + + // Account fetched from the chain state with 900 in its balance. + let receiver_1 = Account::new([6; 8], 900); + + // Account fetched from the chain state with 500 in its balance. + let receiver_2 = Account::new([6; 8], 500); + + let balance_to_move = vec![10, 20]; + + let _inputs_outputs = + nssa::execute_onchain::(&[sender, receiver_1, receiver_2], balance_to_move).unwrap(); + + println!("OK!"); +} diff --git a/risc0-selective-privacy-poc/program_methods/Cargo.toml b/risc0-selective-privacy-poc/program_methods/Cargo.toml new file mode 100644 index 0000000..24c192c --- /dev/null +++ b/risc0-selective-privacy-poc/program_methods/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "program-methods" +version = "0.1.0" +edition = "2021" + +[build-dependencies] +risc0-build = { version = "2.2" } + +[package.metadata.risc0] +methods = ["guest"] diff --git a/risc0-selective-privacy-poc/program_methods/build.rs b/risc0-selective-privacy-poc/program_methods/build.rs new file mode 100644 index 0000000..08a8a4e --- /dev/null +++ b/risc0-selective-privacy-poc/program_methods/build.rs @@ -0,0 +1,3 @@ +fn main() { + risc0_build::embed_methods(); +} diff --git a/risc0-selective-privacy-poc/program_methods/guest/Cargo.toml b/risc0-selective-privacy-poc/program_methods/guest/Cargo.toml new file mode 100644 index 0000000..5263dd9 --- /dev/null +++ b/risc0-selective-privacy-poc/program_methods/guest/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "programs" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +risc0-zkvm = { version = "2.2.0", default-features = false, features = ['std'] } +core = { path = "../../core" } diff --git a/risc0-selective-privacy-poc/program_methods/guest/src/bin/outer.rs b/risc0-selective-privacy-poc/program_methods/guest/src/bin/outer.rs new file mode 100644 index 0000000..bc65957 --- /dev/null +++ b/risc0-selective-privacy-poc/program_methods/guest/src/bin/outer.rs @@ -0,0 +1,157 @@ +use core::{ + check_well_behaved_account_transition, compute_nullifier, hash, is_in_tree, + types::{Nonce, PrivacyExecutionOutput, ProgramId, ProgramOutput}, + visibility::AccountVisibility, +}; +use risc0_zkvm::{guest::env, serde::to_vec}; + +/// Privacy execution logic. +/// This is the circuit for proving correct off-chain executions of programs. +/// It also verifies that the chain's invariants are not violated and the program is well-behaved. +/// +/// Inputs: +/// - ProgramOuptut: The output of the inner program. This is includes the accounts pre and post-states of the execution of the inner program. +/// - Vec: A vector indicating which accounts are private and which are public. +/// - Vec: The vector of nonces to be used for the output accounts. This is assumed to be sampled at random by the host program. +/// - [u32; 8]: The root of the commitment tree. Commitments of input private accounts will be checked against this to prove that they belong to the tree. +/// - ProgamId: The ID of the inner program. +/// +/// Public outputs: +/// - The vector of accounts' pre and post states for the public accounts. +/// - The nullifiers of the used private accounts. +/// - The commitments for the ouput private accounts. +/// - The commitment tree root used for the authentication path verifications. +fn main() { + // Read inner program output + let inner_program_output: ProgramOutput = env::read(); + let num_inputs = inner_program_output.accounts_pre.len(); + + // Read visibilities + let visibilities: Vec = env::read(); + assert_eq!(visibilities.len(), num_inputs); + + // Read nonces for outputs + let output_nonces: Vec = env::read(); + assert_eq!(output_nonces.len(), num_inputs); + + // Read root and program id. + let commitment_tree_root: [u32; 8] = env::read(); + let program_id: ProgramId = env::read(); + + // Authentication step: + let nullifiers = verify_and_nullify_private_inputs(&inner_program_output, &visibilities, commitment_tree_root); + + // Verify pre states and post states of accounts are consistent + // with the execution of the `program_id` program + env::verify(program_id, &to_vec(&inner_program_output).unwrap()).unwrap(); + + // Assert accounts pre- and post-states preserve chains invariants + assert!(check_well_behaved_account_transition( + &inner_program_output.accounts_pre, + &inner_program_output.accounts_post, + program_id + )); + + // From this point on the execution is considered valid + + let output = assemble_privacy_execution_output( + inner_program_output, + visibilities, + output_nonces, + commitment_tree_root, + nullifiers, + ); + + env::commit(&output); +} + +fn assemble_privacy_execution_output( + inner_program_output: ProgramOutput, + visibilities: Vec, + output_nonces: Vec<[u32; 8]>, + commitment_tree_root: [u32; 8], + nullifiers: Vec<[u32; 8]>, +) -> PrivacyExecutionOutput { + // Insert new nonces in private outputs + let accounts_pre = inner_program_output.accounts_pre; + let mut accounts_post = inner_program_output.accounts_post; + accounts_post + .iter_mut() + .zip(output_nonces) + .zip(visibilities.iter()) + .for_each(|((account, new_nonce), visibility)| { + if matches!(visibility, AccountVisibility::Private(_)) { + account.nonce = new_nonce; + } + }); + + // Compute commitments for every private output + let mut private_outputs = Vec::new(); + for (output, visibility) in accounts_post.iter().zip(visibilities.iter()) { + match visibility { + AccountVisibility::Public => continue, + AccountVisibility::Private(_) => private_outputs.push(output), + } + } + let private_output_commitments: Vec<_> = private_outputs.iter().map(|account| account.commitment()).collect(); + + // Get the list of public accounts pre and post states + let mut public_accounts_pre = Vec::new(); + let mut public_accounts_post = Vec::new(); + for ((account_pre, account_post), visibility) in accounts_pre + .into_iter() + .zip(accounts_post.into_iter()) + .zip(visibilities) + { + match visibility { + AccountVisibility::Public => { + public_accounts_pre.push(account_pre); + public_accounts_post.push(account_post); + } + AccountVisibility::Private(_) => continue, + } + } + + PrivacyExecutionOutput { + public_accounts_pre, + public_accounts_post, + private_output_commitments, + nullifiers, + commitment_tree_root, + } +} + +/// Compute nullifiers of private accounts pre states and check that their commitments belong to the commitments tree +fn verify_and_nullify_private_inputs( + inner_program_output: &ProgramOutput, + account_visibilities: &[AccountVisibility], + commitment_tree_root: [u32; 8], +) -> Vec<[u32; 8]> { + let mut nullifiers = Vec::new(); + for (visibility, input_account) in account_visibilities + .iter() + .zip(inner_program_output.accounts_pre.iter()) + { + match visibility { + AccountVisibility::Private(Some((private_key, auth_path))) => { + // Prove ownership of input accounts by proving knowledge of the pre-image of their addresses. + assert_eq!(hash(private_key), input_account.address); + // Check the input account was created by a previous transaction by checking it belongs to the commitments tree. + let commitment = input_account.commitment(); + assert!(is_in_tree(commitment, auth_path, commitment_tree_root)); + // Compute the nullifier to nullify this private input account. + let nullifier = compute_nullifier(&commitment, private_key); + nullifiers.push(nullifier); + } + AccountVisibility::Private(None) => { + // Private accounts without a companion private key are enforced to have default values + // Used for executions that need to create a new private account. + assert_eq!(input_account.balance, 0); + assert_eq!(input_account.nonce, [0; 8]); + } + // No checks on public accounts + AccountVisibility::Public => continue, + } + } + nullifiers +} diff --git a/risc0-selective-privacy-poc/program_methods/guest/src/bin/pinata.rs b/risc0-selective-privacy-poc/program_methods/guest/src/bin/pinata.rs new file mode 100644 index 0000000..49c1e84 --- /dev/null +++ b/risc0-selective-privacy-poc/program_methods/guest/src/bin/pinata.rs @@ -0,0 +1,45 @@ +use core::{account::Account, hash, types::ProgramOutput}; +use risc0_zkvm::guest::env; + +const TARGET_HASH: [u32; 8] = [ + 1363824975, 720119575, 717909014, 2043925380, 717793160, 1495780600, 1253022833, 116132328, +]; +const PINATA_ACCOUNT_ADDR: [u32; 8] = [0xcafe; 8]; +const PINATA_PRIZE: u128 = 100; + +/// A Piñata program +/// To be used both in public and privacy contexts. +fn main() { + // Read input accounts. + // It is expected to receive only two accounts: [pinata_account, winner_account] + let input_accounts: Vec = env::read(); + + // Read claimed preimage + let preimage: Vec = env::read(); + + // Unpack accounts. + assert_eq!(input_accounts.len(), 2); + let [pinata_account, winner_account] = input_accounts.try_into().unwrap(); + + // Check that the given `pinata_account` is correct + assert_eq!(pinata_account.address, PINATA_ACCOUNT_ADDR); + + // Check that the piñata account has enough balance to pay the prize + assert!(pinata_account.balance >= PINATA_PRIZE); + + // Check that the preimage is correct. + assert_eq!(hash(&preimage), TARGET_HASH); + + // Pay the prize + let mut winner_account_post = winner_account.clone(); + let mut pinata_account_post = pinata_account.clone(); + pinata_account_post.balance -= PINATA_PRIZE; + winner_account_post.balance += PINATA_PRIZE; + + let output = ProgramOutput { + accounts_pre: vec![pinata_account, winner_account], + accounts_post: vec![pinata_account_post, winner_account_post], + }; + + env::commit(&output); +} diff --git a/risc0-selective-privacy-poc/program_methods/guest/src/bin/transfer.rs b/risc0-selective-privacy-poc/program_methods/guest/src/bin/transfer.rs new file mode 100644 index 0000000..d600d67 --- /dev/null +++ b/risc0-selective-privacy-poc/program_methods/guest/src/bin/transfer.rs @@ -0,0 +1,31 @@ +use core::{account::Account, types::ProgramOutput}; +use risc0_zkvm::guest::env; + +/// 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: Vec = env::read(); + let balance_to_move: u128 = env::read(); + + // Unpack sender and receiver + assert_eq!(input_accounts.len(), 2); + let [sender, receiver] = input_accounts.try_into().unwrap(); + + // Check sender has enough balance + assert!(sender.balance >= balance_to_move); + + // Create accounts post states, with updated balances + let mut sender_post = sender.clone(); + let mut receiver_post = receiver.clone(); + sender_post.balance -= balance_to_move; + receiver_post.balance += balance_to_move; + + let output = ProgramOutput { + accounts_pre: vec![sender, receiver], + accounts_post: vec![sender_post, receiver_post], + }; + + env::commit(&output); +} diff --git a/risc0-selective-privacy-poc/program_methods/guest/src/bin/transfer_multiple.rs b/risc0-selective-privacy-poc/program_methods/guest/src/bin/transfer_multiple.rs new file mode 100644 index 0000000..bdf1dda --- /dev/null +++ b/risc0-selective-privacy-poc/program_methods/guest/src/bin/transfer_multiple.rs @@ -0,0 +1,44 @@ +use core::{account::Account, types::ProgramOutput}; +use risc0_zkvm::guest::env; + +/// A transfer of balance program with one sender and multiple recipients. +/// To be used both in public and private contexts. +fn main() { + // Read input accounts. + // First account is the sender. + let mut input_accounts: Vec = env::read(); + + // Read the balances to be send to each recipient + let target_balances: Vec = env::read(); + + // Check that there is at least one recipient + assert!(input_accounts.len() > 1); + + // Check that there's one target balance for each recipient. + assert_eq!(target_balances.len() + 1, input_accounts.len()); + + // Unpack sender and recipients + let recipients = input_accounts.split_off(1); + let sender = input_accounts.pop().unwrap(); + + // Check that the sender has enough balance to pay to all recipients + let total_balance_to_move = target_balances.iter().sum(); + assert!(sender.balance >= total_balance_to_move); + + // Create accounts post states, with updated balances + let mut sender_post = sender.clone(); + let mut recipients_post = recipients.clone(); + + // Transfer balances + sender_post.balance -= total_balance_to_move; + for (receiver, balance_for_receiver) in recipients_post.iter_mut().zip(target_balances) { + receiver.balance += balance_for_receiver; + } + + let output = ProgramOutput { + accounts_pre: vec![sender].into_iter().chain(recipients).collect(), + accounts_post: vec![sender_post].into_iter().chain(recipients_post).collect(), + }; + + env::commit(&output); +} diff --git a/risc0-selective-privacy-poc/program_methods/src/lib.rs b/risc0-selective-privacy-poc/program_methods/src/lib.rs new file mode 100644 index 0000000..1bdb308 --- /dev/null +++ b/risc0-selective-privacy-poc/program_methods/src/lib.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/methods.rs")); diff --git a/risc0-selective-privacy-poc/rust-toolchain.toml b/risc0-selective-privacy-poc/rust-toolchain.toml new file mode 100644 index 0000000..36614c3 --- /dev/null +++ b/risc0-selective-privacy-poc/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "stable" +components = ["rustfmt", "rust-src"] +profile = "minimal" diff --git a/risc0-selective-privacy-poc/rustfmt.toml b/risc0-selective-privacy-poc/rustfmt.toml new file mode 100644 index 0000000..7530651 --- /dev/null +++ b/risc0-selective-privacy-poc/rustfmt.toml @@ -0,0 +1 @@ +max_width = 120 diff --git a/risc0-selective-privacy-poc/sparse_merkle_tree/Cargo.toml b/risc0-selective-privacy-poc/sparse_merkle_tree/Cargo.toml new file mode 100644 index 0000000..5e893be --- /dev/null +++ b/risc0-selective-privacy-poc/sparse_merkle_tree/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "sparse-merkle-tree" +version = "0.1.0" +edition = "2021" + +[dependencies] +sha2 = "0.10.9" diff --git a/risc0-selective-privacy-poc/sparse_merkle_tree/src/default_hashes.rs b/risc0-selective-privacy-poc/sparse_merkle_tree/src/default_hashes.rs new file mode 100644 index 0000000..a465f59 --- /dev/null +++ b/risc0-selective-privacy-poc/sparse_merkle_tree/src/default_hashes.rs @@ -0,0 +1,142 @@ +// Values computed as follows +// +// fn default_hashes() -> Vec<[u8; 32]> { +// let mut defaults = vec![ZERO_HASH]; +// for i in 1..TREE_DEPTH { +// let h = hash_node(&defaults[i - 1], &defaults[i - 1]); +// defaults.push(h); +// } +// defaults.into_iter().rev().collect() +// } +// +// +pub(crate) const DEFAULT_HASHES: [[u8; 32]; 32] = [ + [ + 157, 148, 193, 146, 141, 23, 128, 25, 196, 90, 21, 193, 179, 235, 209, 157, 146, 64, 171, + 100, 192, 44, 121, 46, 78, 53, 190, 198, 191, 82, 85, 16, + ], + [ + 244, 159, 70, 55, 214, 43, 252, 179, 167, 206, 218, 26, 170, 158, 22, 127, 12, 155, 130, + 224, 1, 98, 105, 28, 59, 57, 233, 189, 41, 50, 63, 115, + ], + [ + 204, 67, 6, 158, 244, 197, 238, 10, 26, 69, 178, 150, 78, 185, 192, 92, 5, 224, 36, 51, + 115, 21, 130, 142, 229, 223, 102, 195, 57, 198, 17, 27, + ], + [ + 49, 193, 198, 248, 157, 86, 23, 193, 111, 194, 178, 234, 70, 158, 128, 133, 165, 168, 43, + 60, 43, 15, 129, 209, 237, 51, 21, 134, 74, 209, 147, 88, + ], + [ + 219, 156, 83, 145, 70, 138, 195, 224, 161, 165, 121, 148, 215, 154, 104, 234, 219, 0, 91, + 255, 134, 28, 229, 108, 126, 225, 184, 6, 104, 156, 105, 140, + ], + [ + 196, 49, 181, 107, 231, 60, 253, 69, 245, 236, 7, 145, 32, 86, 58, 140, 47, 241, 0, 236, + 104, 61, 176, 23, 170, 95, 128, 66, 179, 134, 192, 209, + ], + [ + 155, 28, 109, 240, 82, 104, 71, 197, 186, 104, 178, 17, 138, 195, 57, 194, 194, 216, 96, + 246, 131, 233, 26, 84, 7, 124, 175, 159, 223, 60, 187, 161, + ], + [ + 183, 22, 254, 169, 215, 123, 104, 4, 156, 9, 23, 45, 110, 238, 115, 162, 108, 188, 142, + 141, 151, 185, 20, 199, 63, 150, 94, 146, 124, 30, 53, 145, + ], + [ + 122, 148, 43, 150, 236, 64, 36, 158, 18, 108, 140, 219, 34, 52, 143, 194, 69, 12, 185, 195, + 88, 206, 30, 249, 126, 255, 18, 221, 99, 72, 18, 91, + ], + [ + 51, 0, 157, 127, 41, 170, 190, 201, 194, 188, 222, 202, 115, 37, 229, 84, 111, 185, 104, + 69, 151, 66, 69, 34, 201, 161, 159, 139, 200, 11, 135, 67, + ], + [ + 186, 234, 98, 18, 205, 31, 46, 119, 118, 209, 66, 20, 180, 72, 129, 169, 242, 250, 48, 128, + 81, 175, 108, 228, 250, 226, 170, 123, 227, 21, 242, 221, + ], + [ + 27, 207, 232, 194, 77, 200, 137, 234, 233, 209, 180, 73, 180, 248, 193, 243, 50, 118, 191, + 199, 245, 30, 142, 242, 28, 234, 249, 134, 195, 154, 138, 162, + ], + [ + 199, 222, 136, 204, 114, 129, 19, 245, 177, 223, 179, 178, 201, 1, 202, 99, 26, 55, 146, + 90, 166, 193, 206, 36, 34, 171, 170, 245, 236, 35, 142, 161, + ], + [ + 121, 214, 101, 193, 197, 86, 227, 248, 59, 227, 3, 15, 20, 191, 124, 129, 209, 226, 93, + 128, 155, 137, 229, 66, 156, 221, 29, 179, 227, 120, 78, 59, + ], + [ + 118, 250, 222, 147, 174, 99, 105, 0, 241, 223, 160, 108, 11, 209, 143, 124, 59, 56, 11, + 164, 127, 2, 3, 18, 236, 149, 4, 176, 167, 196, 138, 245, + ], + [ + 204, 148, 248, 102, 164, 48, 65, 245, 219, 189, 191, 120, 157, 122, 63, 66, 228, 30, 143, + 166, 50, 157, 68, 187, 191, 110, 195, 83, 158, 2, 133, 52, + ], + [ + 179, 199, 88, 222, 194, 63, 148, 195, 88, 33, 190, 181, 102, 109, 100, 199, 212, 19, 198, + 123, 91, 167, 50, 157, 151, 242, 194, 103, 171, 143, 88, 198, + ], + [ + 209, 77, 39, 86, 1, 182, 123, 170, 109, 89, 182, 199, 89, 116, 244, 69, 49, 192, 149, 31, + 156, 226, 106, 73, 2, 112, 161, 78, 75, 153, 68, 189, + ], + [ + 158, 75, 216, 188, 35, 5, 86, 141, 82, 160, 215, 125, 16, 116, 45, 129, 224, 201, 105, 239, + 127, 37, 135, 136, 159, 255, 91, 222, 78, 64, 60, 246, + ], + [ + 121, 98, 24, 197, 183, 169, 52, 200, 156, 241, 142, 73, 241, 171, 113, 215, 133, 250, 13, + 105, 112, 253, 80, 197, 118, 105, 228, 77, 237, 254, 195, 66, + ], + [ + 188, 30, 229, 197, 205, 48, 162, 67, 206, 188, 130, 44, 72, 150, 168, 221, 170, 202, 59, + 110, 83, 205, 9, 10, 130, 11, 129, 79, 5, 218, 164, 97, + ], + [ + 182, 25, 214, 145, 150, 6, 219, 238, 38, 49, 166, 24, 255, 75, 56, 6, 31, 46, 163, 172, + 120, 213, 141, 74, 137, 21, 191, 169, 116, 50, 172, 71, + ], + [ + 52, 69, 229, 255, 230, 237, 127, 41, 223, 116, 249, 52, 228, 220, 231, 233, 38, 66, 188, + 188, 141, 176, 216, 204, 129, 209, 214, 199, 116, 203, 218, 0, + ], + [ + 34, 210, 93, 96, 203, 255, 15, 139, 0, 56, 109, 64, 224, 255, 168, 143, 235, 238, 144, 247, + 57, 237, 244, 210, 215, 160, 98, 250, 108, 101, 127, 130, + ], + [ + 57, 218, 36, 154, 181, 246, 243, 88, 152, 87, 31, 19, 81, 50, 15, 16, 66, 65, 78, 191, 194, + 47, 162, 102, 108, 254, 215, 38, 131, 209, 233, 88, + ], + [ + 155, 30, 22, 245, 84, 30, 111, 118, 197, 124, 53, 108, 138, 34, 183, 149, 93, 161, 54, 20, + 81, 52, 135, 241, 96, 199, 21, 156, 123, 208, 105, 244, + ], + [ + 173, 212, 84, 212, 76, 106, 120, 123, 235, 152, 249, 21, 121, 57, 137, 70, 8, 109, 9, 102, + 153, 66, 109, 61, 116, 176, 20, 123, 52, 240, 173, 143, + ], + [ + 148, 174, 121, 229, 202, 140, 51, 7, 46, 210, 185, 87, 169, 223, 189, 164, 252, 59, 133, + 226, 4, 99, 142, 243, 43, 14, 151, 8, 159, 60, 235, 60, + ], + [ + 175, 132, 242, 248, 185, 9, 188, 62, 34, 213, 240, 199, 176, 177, 75, 99, 187, 215, 70, + 226, 72, 67, 45, 66, 103, 218, 50, 31, 1, 52, 216, 168, + ], + [ + 248, 211, 204, 204, 180, 196, 230, 213, 226, 254, 251, 255, 140, 104, 170, 245, 141, 86, + 82, 142, 59, 109, 142, 191, 7, 180, 33, 12, 239, 230, 161, 241, + ], + [ + 178, 137, 222, 169, 44, 165, 171, 165, 242, 225, 137, 26, 26, 241, 27, 226, 121, 20, 196, + 136, 84, 219, 15, 229, 180, 187, 149, 193, 55, 224, 242, 214, + ], + [ + 110, 52, 11, 156, 255, 179, 122, 152, 156, 165, 68, 230, 187, 120, 10, 44, 120, 144, 29, + 63, 179, 55, 56, 118, 133, 17, 163, 6, 23, 175, 160, 29, + ], +]; diff --git a/risc0-selective-privacy-poc/sparse_merkle_tree/src/lib.rs b/risc0-selective-privacy-poc/sparse_merkle_tree/src/lib.rs new file mode 100644 index 0000000..a4f5528 --- /dev/null +++ b/risc0-selective-privacy-poc/sparse_merkle_tree/src/lib.rs @@ -0,0 +1,211 @@ +mod default_hashes; + +use default_hashes::DEFAULT_HASHES; +use sha2::{Digest, Sha256}; +use std::collections::{HashMap, HashSet}; + +const TREE_DEPTH: usize = 32; +const ONE_HASH: [u8; 32] = [ + 75, 245, 18, 47, 52, 69, 84, 197, 59, 222, 46, 187, 140, 210, 183, 227, 209, 96, 10, 214, 49, 195, 133, 165, 215, + 204, 226, 60, 119, 133, 69, 154, +]; + +/// Compute parent as the hash of two child nodes +fn hash_node(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(left); + hasher.update(right); + hasher.finalize().into() +} + +/// Sparse Merkle Tree with 2^32 leaves +pub struct SparseMerkleTree { + values: HashSet, + node_map: HashMap<(usize, u32), [u8; 32]>, +} + +impl SparseMerkleTree { + pub fn new(values: HashSet) -> Self { + let node_map = Self::node_map(&values); + Self { values, node_map } + } + + pub fn new_empty() -> Self { + Self::new(HashSet::new()) + } + + pub fn add_value(&mut self, new_value: u32) { + if self.values.insert(new_value) { + self.node_map = Self::node_map(&self.values); + } + } + + fn node_map(values: &HashSet) -> HashMap<(usize, u32), [u8; 32]> { + let mut nodes: HashMap<(usize, u32), [u8; 32]> = HashMap::new(); + + // Start from occupied leaves + for &leaf_index in values { + nodes.insert((TREE_DEPTH, leaf_index), ONE_HASH); + } + + // Build tree bottom-up + for depth in (0..TREE_DEPTH).rev() { + let mut next_level = HashMap::new(); + let indices: Vec = nodes + .keys() + .filter(|(d, _)| *d == depth + 1) + .map(|(_, i)| i >> 1) // parent index + .collect(); + + for &parent_index in indices.iter() { + let left_index = parent_index << 1; + let right_index = left_index | 1; + + let left = nodes.get(&(depth + 1, left_index)).unwrap_or(&DEFAULT_HASHES[depth]); + let right = nodes.get(&(depth + 1, right_index)).unwrap_or(&DEFAULT_HASHES[depth]); + + if left != &DEFAULT_HASHES[depth] || right != &DEFAULT_HASHES[depth] { + let h = hash_node(left, right); + next_level.insert((depth, parent_index), h); + } + } + + nodes.extend(next_level); + } + nodes + } + + pub fn root(&self) -> [u8; 32] { + self.node_map.get(&(0, 0)).cloned().unwrap_or(DEFAULT_HASHES[0]) + } + + pub fn get_authentication_path_for_value(&self, value: u32) -> [[u8; 32]; 32] { + let mut path = [[0u8; 32]; 32]; + + let mut current_index = value; + + for depth in (0..32).rev() { + let sibling_index = current_index ^ 1; + + let sibling_hash = self + .node_map + .get(&(depth + 1, sibling_index)) + .cloned() + .unwrap_or(DEFAULT_HASHES[depth]); + + path[31 - depth] = sibling_hash; + current_index >>= 1; + } + + path + } + + pub fn values(&self) -> HashSet { + self.values.clone() + } + + pub fn verify_value_is_in_set(value: u32, path: [[u8; 32]; 32], root: [u8; 32]) -> bool { + let mut hash = ONE_HASH; + let mut current_index = value; + for path_value in path.iter() { + if current_index & 1 == 0 { + hash = hash_node(&hash, path_value); + } else { + hash = hash_node(path_value, &hash); + } + current_index >>= 1; + } + root == hash + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const ZERO_HASH: [u8; 32] = [ + 110, 52, 11, 156, 255, 179, 122, 152, 156, 165, 68, 230, 187, 120, 10, 44, 120, 144, 29, 63, 179, 55, 56, 118, + 133, 17, 163, 6, 23, 175, 160, 29, + ]; + + #[test] + fn test_default_hashes() { + assert_eq!(DEFAULT_HASHES[TREE_DEPTH - 1], ZERO_HASH); + assert_eq!( + DEFAULT_HASHES[0], + [ + 157, 148, 193, 146, 141, 23, 128, 25, 196, 90, 21, 193, 179, 235, 209, 157, 146, 64, 171, 100, 192, 44, + 121, 46, 78, 53, 190, 198, 191, 82, 85, 16 + ] + ); + } + + #[test] + fn test_empty_tree() { + let empty_tree = SparseMerkleTree::new_empty(); + assert_eq!(empty_tree.root(), DEFAULT_HASHES[0]); + } + + #[test] + fn test_tree_1() { + let values: HashSet = vec![0, 1, 2, 3].into_iter().collect(); + let tree = SparseMerkleTree::new(values); + assert_eq!( + tree.root(), + [ + 109, 94, 224, 93, 195, 77, 137, 36, 108, 105, 177, 22, 212, 17, 160, 255, 224, 61, 191, 17, 129, 10, + 26, 76, 197, 42, 230, 160, 80, 44, 101, 184 + ] + ); + } + + #[test] + fn test_tree_2() { + let values: HashSet = vec![2147483648, 2147483649, 2147483650, 2147483651] + .into_iter() + .collect(); + let tree = SparseMerkleTree::new(values); + + assert_eq!( + tree.root(), + [ + 36, 178, 159, 245, 165, 76, 242, 85, 25, 218, 149, 135, 194, 127, 130, 201, 219, 187, 167, 216, 1, 222, + 234, 197, 152, 156, 243, 174, 68, 27, 114, 8 + ] + ); + } + + #[test] + fn test_tree_3() { + let values: HashSet = vec![2147483648, 0, 1, 2147483649].into_iter().collect(); + let tree = SparseMerkleTree::new(values); + + assert_eq!( + tree.root(), + [ + 148, 76, 190, 191, 248, 243, 89, 40, 197, 157, 206, 23, 58, 197, 86, 169, 225, 217, 110, 166, 54, 10, + 245, 175, 168, 4, 145, 220, 30, 210, 67, 113 + ] + ); + } + + #[test] + fn test_auth_path() { + let values: HashSet = vec![0, 1, 2, 3, 1337].into_iter().collect(); + let mut tree = SparseMerkleTree::new(values); + let root = tree.root(); + let path = tree.get_authentication_path_for_value(0); + assert!(SparseMerkleTree::verify_value_is_in_set(0, path, root)); + let path = tree.get_authentication_path_for_value(1); + assert!(SparseMerkleTree::verify_value_is_in_set(1, path, root)); + let path = tree.get_authentication_path_for_value(1337); + assert!(SparseMerkleTree::verify_value_is_in_set(1337, path, root)); + let path = tree.get_authentication_path_for_value(1338); + assert!(!SparseMerkleTree::verify_value_is_in_set(1338, path, root)); + + tree.add_value(1338); + let path = tree.get_authentication_path_for_value(1338); + let root = tree.root(); + assert!(SparseMerkleTree::verify_value_is_in_set(1338, path, root)); + } +} diff --git a/risc0-selective-privacy-poc/src/error.rs b/risc0-selective-privacy-poc/src/error.rs new file mode 100644 index 0000000..69cbd67 --- /dev/null +++ b/risc0-selective-privacy-poc/src/error.rs @@ -0,0 +1,16 @@ +#[derive(Debug)] +pub enum Error { + BadInput, + Risc0(String), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::BadInput => write!(f, "Bad input"), + Error::Risc0(_) => write!(f, "Risc0 error"), + } + } +} + +impl std::error::Error for Error {} diff --git a/risc0-selective-privacy-poc/src/lib.rs b/risc0-selective-privacy-poc/src/lib.rs new file mode 100644 index 0000000..4de1700 --- /dev/null +++ b/risc0-selective-privacy-poc/src/lib.rs @@ -0,0 +1,134 @@ +use core::{ + account::Account, + types::{Nonce, ProgramOutput}, + visibility::AccountVisibility, +}; +use program_methods::{OUTER_ELF, OUTER_ID}; +use rand::{rngs::OsRng, Rng}; +use risc0_zkvm::{default_executor, default_prover, ExecutorEnv, ExecutorEnvBuilder, Receipt}; + +pub mod error; +pub mod program; +pub use error::Error; + +pub use program::Program; + +pub fn new_random_nonce() -> Nonce { + let mut rng = OsRng; + std::array::from_fn(|_| rng.gen()) +} + +/// Writes inputs to `env_builder` in the order expected by the programs +fn write_inputs( + input_accounts: &[Account], + instruction_data: P::InstructionData, + env_builder: &mut ExecutorEnvBuilder, +) -> Result<(), Error> { + let input_accounts = input_accounts.to_vec(); + env_builder.write(&input_accounts).map_err(|_| Error::BadInput)?; + env_builder.write(&instruction_data).map_err(|_| Error::BadInput)?; + Ok(()) +} + +/// Executes and proves the program `P`. +/// Returns the proof and the list of accounts pre and post states +fn execute_and_prove_inner( + input_accounts: &[Account], + instruction_data: P::InstructionData, +) -> Result { + // Write inputs to the program + let mut env_builder = ExecutorEnv::builder(); + write_inputs::

(input_accounts, instruction_data, &mut env_builder)?; + let env = env_builder.build().unwrap(); + + // Prove the program + let prover = default_prover(); + let prove_info = prover + .prove(env, P::PROGRAM_ELF) + .map_err(|e| Error::Risc0(e.to_string()))?; + Ok(prove_info.receipt) +} + +/// Builds the private outputs from the results of the execution of an inner program. +/// Filters private outputs and populates the nonces with the ones provided. +fn build_private_outputs_from_inner_results( + inner_program_output: &ProgramOutput, + visibilities: &[AccountVisibility], + nonces: &[Nonce], +) -> Vec { + inner_program_output + .accounts_post + .iter() + .zip(visibilities) + .zip(nonces) + .filter(|((_, visibility), _)| matches!(visibility, AccountVisibility::Private(_))) + .map(|((account, _), nonce)| { + let mut this = account.clone(); + this.nonce = *nonce; + this + }) + .collect() +} + +/// Executes the program `P` without generating a proof. +/// Returns the list of accounts pre and post states. +pub fn execute_onchain( + input_accounts: &[Account], + instruction_data: P::InstructionData, +) -> Result { + // Write inputs to the program + let mut env_builder = ExecutorEnv::builder(); + write_inputs::

(input_accounts, instruction_data, &mut env_builder)?; + let env = env_builder.build().unwrap(); + + // Execute the program (without proving) + let executor = default_executor(); + let session_info = executor + .execute(env, P::PROGRAM_ELF) + .map_err(|e| Error::Risc0(e.to_string()))?; + + // Get (inputs and) outputs + session_info.journal.decode().map_err(|e| Error::Risc0(e.to_string())) +} + +/// Executes and proves the inner program `P` and executes and proves the outer program on top of it. +/// Returns the proof of execution of the outer program and the list of new private accounts +/// resulted from this execution. +pub fn execute_offchain( + inputs: &[Account], + instruction_data: P::InstructionData, + visibilities: &[AccountVisibility], + commitment_tree_root: [u32; 8], +) -> Result<(Receipt, Vec), Error> { + // Prove inner program and get post state of the accounts + let inner_receipt = execute_and_prove_inner::

(inputs, instruction_data)?; + let inner_program_output: ProgramOutput = inner_receipt + .journal + .decode() + .map_err(|e| Error::Risc0(e.to_string()))?; + + // Sample fresh random nonces for the outputs of this execution + let output_nonces: Vec<_> = (0..inputs.len()).map(|_| new_random_nonce()).collect(); + + // 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)) +} + +/// Verifies a proof of the outer program for the given parameters. +pub fn verify_privacy_execution(receipt: Receipt) -> Result<(), Error> { + receipt.verify(OUTER_ID).map_err(|e| Error::Risc0(e.to_string())) +} diff --git a/risc0-selective-privacy-poc/src/program/mod.rs b/risc0-selective-privacy-poc/src/program/mod.rs new file mode 100644 index 0000000..964e3db --- /dev/null +++ b/risc0-selective-privacy-poc/src/program/mod.rs @@ -0,0 +1,35 @@ +use core::types::ProgramId; + +use program_methods::{PINATA_ELF, PINATA_ID, TRANSFER_ELF, TRANSFER_ID, TRANSFER_MULTIPLE_ELF, TRANSFER_MULTIPLE_ID}; +use serde::{Deserialize, Serialize}; + +/// A trait to be implemented by inner programs. +pub trait Program { + const PROGRAM_ID: ProgramId; + const PROGRAM_ELF: &[u8]; + type InstructionData: Serialize + for<'de> Deserialize<'de>; +} + +pub struct TransferProgram; +impl Program for TransferProgram { + const PROGRAM_ID: ProgramId = TRANSFER_ID; + const PROGRAM_ELF: &[u8] = TRANSFER_ELF; + /// Amount to transfer + type InstructionData = u128; +} + +pub struct TransferMultipleProgram; +impl Program for TransferMultipleProgram { + const PROGRAM_ID: ProgramId = TRANSFER_MULTIPLE_ID; + const PROGRAM_ELF: &[u8] = TRANSFER_MULTIPLE_ELF; + /// Amounts to transfer + type InstructionData = Vec; +} + +pub struct PinataProgram; +impl Program for PinataProgram { + const PROGRAM_ID: ProgramId = PINATA_ID; + const PROGRAM_ELF: &[u8] = PINATA_ELF; + /// Preimage of target hash to win prize + type InstructionData = Vec; +}