Merge pull request #6 from vacp2p/schouhy/selective-privacy-poc

Add Selective Privacy POC with Risc0
This commit is contained in:
Sergio Chouhy
2025-07-23 16:59:36 -03:00
committed by GitHub
36 changed files with 1752 additions and 1 deletions

5
.gitignore vendored
View File

@@ -1,2 +1,5 @@
target
Cargo.lock
Cargo.lock
risc0-selective-privacy-poc/target
risc0-selective-privacy-poc/Cargo.lock
risc0-selective-privacy-poc/methods/guest/Cargo.lock

View File

@@ -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"]

View File

@@ -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 }

View File

@@ -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<ProgramId>,
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]
}
}

View File

@@ -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::<HashSet<_>>()
.len()
!= input_accounts.len()
{
return false;
}
true
}

View File

@@ -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<Account>,
pub accounts_post: Vec<Account>,
}
#[derive(Serialize, Deserialize)]
pub struct PrivacyExecutionOutput {
pub public_accounts_pre: Vec<Account>,
pub public_accounts_post: Vec<Account>,
pub private_output_commitments: Vec<Commitment>,
pub nullifiers: Vec<Nullifier>,
pub commitment_tree_root: [u32; 8],
}

View File

@@ -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)>),
}

View File

@@ -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::<PinataProgram>(&[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::<PinataProgram>(
&[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!");
}

View File

@@ -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<P: nssa::Program>(
input_accounts: &[Account],
instruction_data: P::InstructionData,
visibilities: &[AccountVisibility],
commitment_tree_root: [u32; 8],
sequencer: &mut MockedSequencer,
) -> Result<Vec<Account>, Error> {
// Execute and generate proof of the outer program
let (receipt, private_outputs) =
nssa::execute_offchain::<P>(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)
}
}

View File

@@ -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<Account, 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(&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::<TransferProgram>(
&[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)
}
}

View File

@@ -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::<TransferProgram>(
&[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])
}
}

View File

@@ -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::<TransferProgram>(&[self.user_address(), *to_address], amount_to_transfer)
.map_err(|_| Error::BadInput)
}
}

View File

@@ -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<Account, Error> {
// 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::<TransferProgram>(
&[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)
}
}

View File

@@ -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]),
];

View File

@@ -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 {}

View File

@@ -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<Address, Account>,
commitment_tree: SparseMerkleTree,
nullifier_set: HashSet<Nullifier>,
}
/// 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<Address, Account> = 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<Account> {
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::<Vec<_>>()
.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<String> = 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");
}

View File

@@ -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 &current_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(())
}
}

View File

@@ -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<P: nssa::Program>(
&mut self,
input_account_addresses: &[Address],
instruction_data: P::InstructionData,
) -> Result<(), Error> {
// Fetch the current state of the input accounts.
let input_accounts: Vec<Account> = input_account_addresses
.iter()
.map(|address| self.get_account(address).ok_or(Error::NotFound))
.collect::<Result<_, _>>()?;
// Execute the program
let program_output =
nssa::execute_onchain::<P>(&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(())
}
}

View File

@@ -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 <Npk> 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::<TransferMultipleProgram>(
&[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)
}

View File

@@ -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::<TransferMultipleProgram>(&[sender, receiver_1, receiver_2], balance_to_move).unwrap();
println!("OK!");
}

View File

@@ -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"]

View File

@@ -0,0 +1,3 @@
fn main() {
risc0_build::embed_methods();
}

View File

@@ -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" }

View File

@@ -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<AccountVisibility>: A vector indicating which accounts are private and which are public.
/// - Vec<Nonce>: 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<AccountVisibility> = env::read();
assert_eq!(visibilities.len(), num_inputs);
// Read nonces for outputs
let output_nonces: Vec<Nonce> = 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<AccountVisibility>,
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
}

View File

@@ -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<Account> = env::read();
// Read claimed preimage
let preimage: Vec<u32> = 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);
}

View File

@@ -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<Account> = 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);
}

View File

@@ -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<Account> = env::read();
// Read the balances to be send to each recipient
let target_balances: Vec<u128> = 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);
}

View File

@@ -0,0 +1 @@
include!(concat!(env!("OUT_DIR"), "/methods.rs"));

View File

@@ -0,0 +1,4 @@
[toolchain]
channel = "stable"
components = ["rustfmt", "rust-src"]
profile = "minimal"

View File

@@ -0,0 +1 @@
max_width = 120

View File

@@ -0,0 +1,7 @@
[package]
name = "sparse-merkle-tree"
version = "0.1.0"
edition = "2021"
[dependencies]
sha2 = "0.10.9"

View File

@@ -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,
],
];

View File

@@ -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<u32>,
node_map: HashMap<(usize, u32), [u8; 32]>,
}
impl SparseMerkleTree {
pub fn new(values: HashSet<u32>) -> 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<u32>) -> 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<u32> = 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<u32> {
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<u32> = 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<u32> = 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<u32> = 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<u32> = 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));
}
}

View File

@@ -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 {}

View File

@@ -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<P: Program>(
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<P: Program>(
input_accounts: &[Account],
instruction_data: P::InstructionData,
) -> Result<Receipt, Error> {
// Write inputs to the program
let mut env_builder = ExecutorEnv::builder();
write_inputs::<P>(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<Account> {
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<P: Program>(
input_accounts: &[Account],
instruction_data: P::InstructionData,
) -> Result<ProgramOutput, Error> {
// Write inputs to the program
let mut env_builder = ExecutorEnv::builder();
write_inputs::<P>(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<P: Program>(
inputs: &[Account],
instruction_data: P::InstructionData,
visibilities: &[AccountVisibility],
commitment_tree_root: [u32; 8],
) -> Result<(Receipt, Vec<Account>), Error> {
// Prove inner program and get post state of the accounts
let inner_receipt = execute_and_prove_inner::<P>(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()))
}

View File

@@ -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<u128>;
}
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<u32>;
}