Private transactions

In this implementation, we'll achieve privacy with FHE. Any private values (balances, transaction amounts) will be encrypted as ciphertexts, and transparent computation will rely on homomorphic FHE programs.1 We'll prove the correctness and validity of these private values with linked ZKPs.

Let's get to it!

Program walkthrough

The complete example lives on GitHub if you want to see it altogether.

Setup

First, let's import everything we need:

use std::collections::HashMap;

use sunscreen::{
    bulletproofs::BulletproofsBackend,
    fhe_program,
    linked::LinkedProof,
    types::{
        bfv::Signed,
        zkp::{
            AsFieldElement, BfvSigned, BulletproofsField, ConstrainCmp, ConstrainFresh, Field,
            FieldSpec,
        },
        Cipher,
    },
    zkp_program, zkp_var, Ciphertext, CompiledFheProgram, CompiledZkpProgram, Compiler,
    FheProgramInput, FheZkpApplication, FheZkpRuntime, Params, PrivateKey, PublicKey, Result,
    ZkpProgramInput,
};

FHE programs

The FHE programs are mostly trivial. We definitely need addition and subtraction to update user's balances, and these are performed on encrypted values. In addition, in our implementation, we'll assume that users are going to deposit into their private accounts from a public account (perhaps the native currency of a blockchain, like ETH), so we'll make use of the fact that we can perform addition on mixed ciphertext and plaintext values.

/// Subtract the transaction amount from the sender's balance.
#[fhe_program(scheme = "bfv")]
fn transfer_from(balance: Cipher<Signed>, tx: Cipher<Signed>) -> Cipher<Signed> {
    balance - tx
}

/// Add the transaction amount to the receiver's balance.
#[fhe_program(scheme = "bfv")]
fn transfer_to(balance: Cipher<Signed>, tx: Cipher<Signed>) -> Cipher<Signed> {
    balance + tx
}

/// Add the public transaction amount to a user's balance.
#[fhe_program(scheme = "bfv")]
fn deposit_to(balance: Cipher<Signed>, deposit: Signed) -> Cipher<Signed> {
    balance + deposit
}

ZKP programs

Transfer

Let's first consider what constitutes a valid transfer. Since we need to add the transaction amount to both the sender and the receiver's balance, we actually need two ciphertexts, one encrypted under the sender's key and the other under the receiver's key. We'll need to prove that

  1. the sender has enough funds to send the tx amount
  2. the tx amount is positive
  3. the ciphertexts encrypt the same amount
  4. the ciphertexts are fresh encryptions

The first three are rather obvious requirements for the correctness of the payment system, but the last one is more subtle. We need to ensure these are fresh encryptions because BFV doesn't have unbounded computation depth. We wouldn't want a bad actor to be able to send an encrypted transaction with a ton of noise that causes the receiver's balance to be un-decryptable.

As we'll see below, the last two properties are handled outside of the ZKP program (by the SDLP), so let's validate the first two properties.

/// Validate a transfer transaction.
#[zkp_program]
fn validate_transfer<F: FieldSpec>(
    #[linked] tx: BfvSigned<F>,
    #[linked] sender_balance: BfvSigned<F>,
) {
    let tx = tx.into_field_elem();
    let sender_balance = sender_balance.into_field_elem();

    // Transaction amount must be greater than 0.
    tx.constrain_gt_bounded(zkp_var!(0), 64);
    // Transaction amount cannot exceed sender's balance.
    tx.constrain_le_bounded(sender_balance, 64);
}

Registration

As we noted above, we're assuming a deposit to a private account occurs from a public account. But the user's balance must be encrypted, so how can we initialize it? We can't easily encrypt the initial deposit, at least in a consensus-driven setting, as encryptions are randomized. Instead, we'll have the user send over their encrypted initial balance with a ZKP proving that the encrypted amount is equal to the public deposit.

/// Validate registration. The deposit amount is public, but we must prove that the provided
/// ciphertext encrypts the deposit amount.
#[zkp_program]
fn validate_registration<F: FieldSpec>(
    #[linked] encrypted_deposit: BfvSigned<F>,
    #[public] public_deposit: Field<F>,
) {
    let encrypted_deposit = encrypted_deposit.into_field_elem();
    encrypted_deposit.constrain_eq(public_deposit);
}

Refresh balance

To really make this example realistic, we're including a balance refresh operation. We refresh a balance so that

  1. the ciphertext doesn't overflow its noise budget
  2. the encrypted plaintext doesn't overflow its plaintext modulus

We need to prove that the fresh balance does indeed have a fresh encoding, and that it encrypts the same value as the existing one.2

/// Validate a balance refresh. We must prove that the two values are equal and that the fresh
/// balance is freshly encoded.
#[zkp_program]
fn validate_refresh_balance<F: FieldSpec>(
    #[linked] existing_balance: BfvSigned<F>,
    #[linked] fresh_balance: BfvSigned<F>,
) {
    fresh_balance.constrain_fresh_encoding();
    fresh_balance
        .into_field_elem()
        .constrain_eq(existing_balance.into_field_elem());
}

App

For convenience, we'll wrap up the FHE and ZKP programs into an application type, this way each party can instantiate the same programs and run operations with the same paramaters.

pub struct App(FheZkpApplication);

impl App {
    pub fn new() -> Result<Self> {
        let app = Compiler::new()
            .fhe_program(transfer_to)
            .fhe_program(transfer_from)
            .fhe_program(deposit_to)
            .zkp_backend::<BulletproofsBackend>()
            .zkp_program(validate_transfer)
            .zkp_program(validate_registration)
            .zkp_program(validate_refresh_balance)
            .compile()?;
        Ok(Self(app))
    }

    pub fn get_transfer_zkp(&self) -> &CompiledZkpProgram {
        self.0.get_zkp_program(validate_transfer).unwrap()
    }

    pub fn get_registration_zkp(&self) -> &CompiledZkpProgram {
        self.0.get_zkp_program(validate_registration).unwrap()
    }

    pub fn get_refresh_balance_zkp(&self) -> &CompiledZkpProgram {
        self.0.get_zkp_program(validate_refresh_balance).unwrap()
    }

    pub fn get_transfer_to_fhe(&self) -> &CompiledFheProgram {
        self.0.get_fhe_program(transfer_to).unwrap()
    }

    pub fn get_transfer_from_fhe(&self) -> &CompiledFheProgram {
        self.0.get_fhe_program(transfer_from).unwrap()
    }

    pub fn get_deposit_to_fhe(&self) -> &CompiledFheProgram {
        self.0.get_fhe_program(deposit_to).unwrap()
    }

    pub fn params(&self) -> &Params {
        self.0.params()
    }
}

Transactions

Since we're imagining a blockchain like setting, users will act by sending atomic transactions to the chain. Let's piece together what the transaction types will look like.

Transfer

Recall a user needs to send over two ciphertexts encrypting the transaction amount. Of course, they'll also need to send the validity proof and a way to identify the sender and receiver.

/// A way to identify a user.
type Username = String;

/// A private transfer transaction.
///
/// The SDLP in the linked proof proves that the ciphertexts are valid, fresh encryptions of the
/// same value. The R1CS ZKP in the linked proof proves that the amount encrypted does not exceed
/// the sender's current balance.
#[derive(Clone)]
pub struct Transfer {
    proof: LinkedProof,
    // Transfer amount encrypted under sender's key
    encrypted_amount_sender: Ciphertext,
    // Transfer amount encrypted under receiver's key
    encrypted_amount_receiver: Ciphertext,
    sender: Username,
    receiver: Username,
}

Deposit

A registration will rely on a deposit, so let's define this type first. Since the amount is public, depositing into an existing account doesn't have any proof requirements.

/// A public deposit transaction.
#[derive(Clone)]
pub struct Deposit {
    public_amount: i64,
    name: Username,
}

Registration

As mentioned, the registration is an initial deposit with a matching initial encrypted balance. In addition, the computing party needs to know the user's public key to run FHE programs on their ciphertexts.

/// A register transaction.
///
/// The SDLP in the linked proof proves that the ciphertext is a valid, fresh encryption. The R1CS
/// ZKP in the linked proof proves that the amount encrypted matches the public amount deposited.
#[derive(Clone)]
pub struct Register {
    proof: LinkedProof,
    public_key: PublicKey,
    encrypted_amount: Ciphertext,
    deposit: Deposit,
}

Refresh balance

Lastly, refreshing a balance requires the new ciphertext and its accompanying proof of validity.

/// A refresh private balance transaction.
///
/// The SDLP in the linked proof proves that the fresh balance is a valid, fresh encryption (to
/// avoid overflowing the noise budget). The R1CS ZKP in the linked proof proves that the new
/// encryption is also _freshly encoded_ (to avoid overflowing the plaintext modulus) and that it
/// matches the existing value on chain.
#[derive(Clone)]
pub struct RefreshBalance {
    proof: LinkedProof,
    fresh_balance: Ciphertext,
    name: Username,
}

Parties

Let's first define the different parties. Well have User struct for parties that wish to use the private transactions system, and we'll have a Chain struct for the computing party, in this case mimicking a blockchain.

/// Perspective of a user.
pub struct User {
    pub name: Username,
    pub public_key: PublicKey,
    private_key: PrivateKey,
    // This app holds ZKP programs used to make proofs
    app: App,
    // The runtime is used for encryption/decryption and creating proofs
    runtime: FheZkpRuntime<BulletproofsBackend>,
}

impl User {
    pub fn new(name: &str) -> Result<Self> {
        let app = App::new()?;
        let runtime = FheZkpRuntime::new(app.params(), &BulletproofsBackend::new())?;
        let (public_key, private_key) = runtime.generate_keys()?;
        Ok(Self {
            name: name.to_string(),
            runtime,
            public_key,
            private_key,
            app,
        })
    }
}

/// A chain transaction.
pub enum Transaction {
    Register(Register),
    Deposit(Deposit),
    Transfer(Transfer),
    RefreshBalance(RefreshBalance),
}

/// Perspective of the blockchain, basically just a place where user's encrypted balances are
/// stored and transparent FHE computations take place in the form of atomic transactions.
///
/// In this simple example, assume read-only references `&Chain` provide "call" functionalities,
/// i.e. non-mutating methods for reading chain data, and mutable references `&mut Chain` provide
/// "send" functionalities, i.e. sending transactions that can mutate chain data.
pub struct Chain {
    /// The current balances
    balances: HashMap<Username, Ciphertext>,
    /// The user's public keys
    keys: HashMap<Username, PublicKey>,
    /// Ledger of transactions
    ledger: Vec<Transaction>,
    /// App holding FHE and ZKP programs
    app: App,
    /// Runtime to run FHE programs and verify proofs
    runtime: FheZkpRuntime<BulletproofsBackend>,
}

impl Chain {
    pub fn new() -> Result<Self> {
        let app = App::new()?;
        let runtime = FheZkpRuntime::new(app.params(), &BulletproofsBackend::new())?;
        Ok(Self {
            balances: HashMap::new(),
            keys: HashMap::new(),
            ledger: Vec::new(),
            runtime,
            app,
        })
    }
}

User

Now let's go over the user's perspective and how they'll construct the various transactions.

Registration

To register, a user will use the LinkedProofBuilder to encrypt their initial deposit and link it to the ZKP proving its equality to the public amount. We'll also add some print statements in so that we can watch what happens when we run main below.

impl User {
    /// Create a public deposit to a private balance.
    pub fn create_deposit(&self, amount: i64) -> Deposit {
        Deposit {
            public_amount: amount,
            name: self.name.clone(),
        }
    }

    /// Create a register transaction.
    pub fn create_register(&self, initial_deposit: i64) -> Result<Register> {
        let mut builder = self.runtime.linkedproof_builder();

        // Encrypt deposit amount
        println!(
            "    {}: encrypting and linking {}",
            self.name, initial_deposit
        );
        let (amount_enc, amount_linked) =
            builder.encrypt_returning_link(&Signed::from(initial_deposit), &self.public_key)?;

        // Create registration proof
        println!("    {}: creating registration linkedproof", self.name);
        let proof = builder
            .zkp_program(self.app.get_registration_zkp())?
            .linked_input(amount_linked)
            .public_input(BulletproofsField::from(initial_deposit))
            .build()?;

        Ok(Register {
            proof,
            encrypted_amount: amount_enc,
            public_key: self.public_key.clone(),
            deposit: self.create_deposit(initial_deposit),
        })
    }
}

Transfer

To create a transfer, the user needs to encrypt the transaction under the receiver's public key; they can read this off the chain, since registered users will have their public keys stored there. They also will need to read off their current encrypted balance to link it to the validate_transfer proof.

Here, we'll make use of some of the more exotic methods of the LinkedProofBuilder; after calling encrypt_returning_link to link the transaction amount to the ZKP, we'll call reencrypt which implicitly proves that the returned ciphertexts encrypt the same plaintext message. Both of these methods also implicitly prove that the returned ciphertexts are fresh encryptions. Finally we'll call decrypt_returning_link to link the current balance to the ZKP.

impl User {
    /// Create a private, validated transfer to send to another user.
    pub fn create_transfer<U: Into<Username>>(
        &self,
        chain: &Chain,
        amount: i64,
        receiver: U,
    ) -> Result<Transfer> {
        let receiver = receiver.into();
        let mut builder = self.runtime.linkedproof_builder();

        // Encrypt tx amount under sender's public key.
        println!("    {}: encrypting {} under own key", self.name, amount);
        let (encrypted_amount_sender, amount_linked) =
            builder.encrypt_returning_link(&Signed::from(amount), &self.public_key)?;

        // Encrypt tx amount under receiver's public key, implicitly proving that the two
        // ciphertexts encrypt the same value.
        println!(
            "    {}: encrypting {} under receiver key",
            self.name, amount
        );
        let recv_pk = chain.keys.get(&receiver).unwrap();
        let encrypted_amount_receiver = builder.reencrypt(&amount_linked, recv_pk)?;

        // Decrypt current balance, needed to prove tx validity
        let balance_enc = chain.balances.get(&self.name).unwrap();
        let (balance, balance_linked) =
            builder.decrypt_returning_link::<Signed>(balance_enc, &self.private_key)?;

        // Create transfer proof
        println!(
            "    {}: creating transfer linkedproof, proving {} <= {}",
            self.name, amount, balance
        );
        let proof = builder
            .zkp_program(self.app.get_transfer_zkp())?
            .linked_input(amount_linked)
            .linked_input(balance_linked)
            .build()?;

        Ok(Transfer {
            proof,
            sender: self.name.clone(),
            receiver,
            encrypted_amount_sender,
            encrypted_amount_receiver,
        })
    }
}

Refresh balance

Refreshing a balance requires linking both the fresh encryption and the existing ciphertext. We'll again read the existing ciphertext off the chain.

impl User {
    /// Create a refresh balance transaction.
    pub fn create_refresh_balance(&self, chain: &Chain) -> Result<RefreshBalance> {
        let mut builder = self.runtime.linkedproof_builder();

        // Decrypt current balance, returning a link to the underlying message
        let balance_encrypted = chain.balances.get(&self.name).unwrap();
        let (balance, existing_link) =
            builder.decrypt_returning_link::<Signed>(balance_encrypted, &self.private_key)?;

        // Re-encrypt the current balance, returning a link to the underlying message
        println!("    {}: re-encrypting balance of {}", self.name, balance);
        let (fresh_balance, fresh_link) =
            builder.encrypt_returning_link(&balance, &self.public_key)?;

        // Generate proof that the ciphertexts encrypt the same underlying message and that
        // the new one has a fresh noise budget and fresh encoding.
        println!("    {}: creating refresh balance linkedproof", self.name);
        let proof = builder
            .zkp_program(self.app.get_refresh_balance_zkp())?
            .linked_input(existing_link)
            .linked_input(fresh_link)
            .build()?;

        Ok(RefreshBalance {
            proof,
            fresh_balance,
            name: self.name.clone(),
        })
    }
}

Astute readers may have noticed that we've proven ciphertext equality within the ZKP program, rather than calling builder.reencrypt(existing_link) as we did for the transfer linked proof. By calling encrypt_returning_link we are creating a new freshly encoded plaintext, and then creating a freshly encrypted ciphertext of it. The reencrypt method does not create a freshly encoded plaintext, rather it re-encrypts the exact plaintext of the existing message. (And since our ZKP program constrains the linked value to a fresh encoding, this would fail for anything but initial balances.)

Chain

Next we'll show how the chain will process these transactions.

Registration

Here's how the chain will verify a registration and, if successful, update its state.

impl Chain {
    pub fn register(&mut self, register: Register) -> Result<()> {
        self.ledger.push(Transaction::Register(register.clone()));
        let Register {
            proof,
            encrypted_amount,
            public_key,
            deposit,
        } = register;

        // First, verify that the encrypted amount matches the public amount
        let mut builder = self.runtime.linkedproof_verification_builder();
        builder.encrypt_returning_link::<Signed>(&encrypted_amount, &public_key)?;
        builder
            .zkp_program(self.app.get_registration_zkp())?
            .proof(proof)
            .public_input(BulletproofsField::from(deposit.public_amount))
            .verify()?;

        // Register the user's public key
        self.keys.insert(deposit.name.clone(), public_key);

        // Set the initial encrypted balance
        self.balances.insert(deposit.name, encrypted_amount);
        Ok(())
    }
}

Deposit

Once a user is registered, they can make more deposits. The chain will use the deposit_to FHE program, adding the public plaintext amount to the encrypted balance.

impl Chain {
    pub fn deposit(&mut self, deposit: Deposit) -> Result<()> {
        self.ledger.push(Transaction::Deposit(deposit.clone()));
        let Deposit {
            public_amount,
            name,
        } = deposit;

        // Deposit into the user's balance
        let pk = self.keys.get(&name).unwrap();
        let curr_bal = self.balances.get_mut(&name).unwrap();
        *curr_bal = self
            .runtime
            .run::<FheProgramInput>(
                self.app.get_deposit_to_fhe(),
                vec![curr_bal.clone().into(), Signed::from(public_amount).into()],
                pk,
            )?
            .remove(0);
        Ok(())
    }
}

Transfer

For a private transfer, the chain needs to verify the inputs by verifying the accompanying proof, and then run two FHE programs, one for the sender and one for the receiver.

impl Chain {
    pub fn transfer(&mut self, transfer: Transfer) -> Result<()> {
        self.ledger.push(Transaction::Transfer(transfer.clone()));
        let Transfer {
            proof,
            encrypted_amount_sender,
            encrypted_amount_receiver,
            sender,
            receiver,
        } = transfer;

        // First verify the transfer is valid
        let mut builder = self.runtime.linkedproof_verification_builder();
        let link = builder.encrypt_returning_link::<Signed>(
            &encrypted_amount_sender,
            self.keys.get(&sender).unwrap(),
        )?;
        builder.reencrypt(
            &link,
            &encrypted_amount_receiver,
            self.keys.get(&receiver).unwrap(),
        )?;
        builder.decrypt_returning_link::<Signed>(self.balances.get(&sender).unwrap())?;
        builder
            .zkp_program(self.app.get_transfer_zkp())?
            .proof(proof)
            .verify()?;

        // Update the sender's balance:
        let sender_pk = self.keys.get(&sender).unwrap();
        let sender_balance = self.balances.get_mut(&sender).unwrap();
        *sender_balance = self
            .runtime
            .run(
                self.app.get_transfer_from_fhe(),
                vec![sender_balance.clone(), encrypted_amount_sender],
                sender_pk,
            )?
            .remove(0);

        // Update receiver's balance
        let receiver_pk = self.keys.get(&receiver).unwrap();
        let receiver_balance = self.balances.get_mut(&receiver).unwrap();
        *receiver_balance = self
            .runtime
            .run(
                self.app.get_transfer_to_fhe(),
                vec![receiver_balance.clone(), encrypted_amount_receiver],
                receiver_pk,
            )?
            .remove(0);
        Ok(())
    }
}

Refresh balance

Finally, to refresh a balance the chain simply needs to verify the proof and then overwrite the existing balance.

impl Chain {
    pub fn refresh_balance(&mut self, refresh_balance: RefreshBalance) -> Result<()> {
        self.ledger
            .push(Transaction::RefreshBalance(refresh_balance.clone()));
        let RefreshBalance {
            proof,
            fresh_balance,
            name,
        } = refresh_balance;

        // Verify the balance refresh is valid
        let mut builder = self.runtime.linkedproof_verification_builder();
        builder.decrypt_returning_link::<Signed>(self.balances.get(&name).unwrap())?;
        builder.encrypt_returning_link::<Signed>(&fresh_balance, self.keys.get(&name).unwrap())?;
        builder
            .zkp_program(self.app.get_refresh_balance_zkp())?
            .proof(proof)
            .verify()?;

        // Use the freshly encrypted balance
        self.balances
            .insert(name, fresh_balance)
            .expect("User should be registered");
        Ok(())
    }
}

Run it!

Finally, here's a runnable main function demonstrating the transactions above:

use std::collections::HashMap;

use sunscreen::{
    bulletproofs::BulletproofsBackend,
    fhe_program,
    linked::LinkedProof,
    types::{
        bfv::Signed,
        zkp::{
            AsFieldElement, BfvSigned, BulletproofsField, ConstrainCmp, ConstrainFresh, Field,
            FieldSpec,
        },
        Cipher,
    },
    zkp_program, zkp_var, Ciphertext, CompiledFheProgram, CompiledZkpProgram, Compiler,
    FheProgramInput, FheZkpApplication, FheZkpRuntime, Params, PrivateKey, PublicKey, Result,
    ZkpProgramInput,
};

/// Subtract the transaction amount from the sender's balance.
#[fhe_program(scheme = "bfv")]
fn transfer_from(balance: Cipher<Signed>, tx: Cipher<Signed>) -> Cipher<Signed> {
    balance - tx
}

/// Add the transaction amount to the receiver's balance.
#[fhe_program(scheme = "bfv")]
fn transfer_to(balance: Cipher<Signed>, tx: Cipher<Signed>) -> Cipher<Signed> {
    balance + tx
}

/// Add the public transaction amount to a user's balance.
#[fhe_program(scheme = "bfv")]
fn deposit_to(balance: Cipher<Signed>, deposit: Signed) -> Cipher<Signed> {
    balance + deposit
}

/// Validate a transfer transaction.
#[zkp_program]
fn validate_transfer<F: FieldSpec>(
    #[linked] tx: BfvSigned<F>,
    #[linked] sender_balance: BfvSigned<F>,
) {
    let tx = tx.into_field_elem();
    let sender_balance = sender_balance.into_field_elem();

    // Transaction amount must be greater than 0.
    tx.constrain_gt_bounded(zkp_var!(0), 64);
    // Transaction amount cannot exceed sender's balance.
    tx.constrain_le_bounded(sender_balance, 64);
}

/// Validate registration. The deposit amount is public, but we must prove that the provided
/// ciphertext encrypts the deposit amount.
#[zkp_program]
fn validate_registration<F: FieldSpec>(
    #[linked] encrypted_deposit: BfvSigned<F>,
    #[public] public_deposit: Field<F>,
) {
    let encrypted_deposit = encrypted_deposit.into_field_elem();
    encrypted_deposit.constrain_eq(public_deposit);
}

/// Validate a balance refresh. We must prove that the two values are equal and that the fresh
/// balance is freshly encoded.
#[zkp_program]
fn validate_refresh_balance<F: FieldSpec>(
    #[linked] existing_balance: BfvSigned<F>,
    #[linked] fresh_balance: BfvSigned<F>,
) {
    fresh_balance.constrain_fresh_encoding();
    fresh_balance
        .into_field_elem()
        .constrain_eq(existing_balance.into_field_elem());
}

/// A way to identify a user.
type Username = String;

/// Perspective of a user.
pub struct User {
    pub name: Username,
    pub public_key: PublicKey,
    private_key: PrivateKey,
    // This app holds ZKP programs used to make proofs
    app: App,
    // The runtime is used for encryption/decryption and creating proofs
    runtime: FheZkpRuntime<BulletproofsBackend>,
}

impl User {
    pub fn new(name: &str) -> Result<Self> {
        let app = App::new()?;
        let runtime = FheZkpRuntime::new(app.params(), &BulletproofsBackend::new())?;
        let (public_key, private_key) = runtime.generate_keys()?;
        Ok(Self {
            name: name.to_string(),
            runtime,
            public_key,
            private_key,
            app,
        })
    }

    /// Create a private, validated transfer to send to another user.
    pub fn create_transfer<U: Into<Username>>(
        &self,
        chain: &Chain,
        amount: i64,
        receiver: U,
    ) -> Result<Transfer> {
        let receiver = receiver.into();
        let mut builder = self.runtime.linkedproof_builder();

        // Encrypt tx amount under sender's public key.
        println!("    {}: encrypting {} under own key", self.name, amount);
        let (encrypted_amount_sender, amount_linked) =
            builder.encrypt_returning_link(&Signed::from(amount), &self.public_key)?;

        // Encrypt tx amount under receiver's public key, implicitly proving that the two
        // ciphertexts encrypt the same value.
        println!(
            "    {}: encrypting {} under receiver key",
            self.name, amount
        );
        let recv_pk = chain.keys.get(&receiver).unwrap();
        let encrypted_amount_receiver = builder.reencrypt(&amount_linked, recv_pk)?;

        // Decrypt current balance, needed to prove tx validity
        let balance_enc = chain.balances.get(&self.name).unwrap();
        let (balance, balance_linked) =
            builder.decrypt_returning_link::<Signed>(balance_enc, &self.private_key)?;

        // Create transfer proof
        println!(
            "    {}: creating transfer linkedproof, proving {} <= {}",
            self.name, amount, balance
        );
        let proof = builder
            .zkp_program(self.app.get_transfer_zkp())?
            .linked_input(amount_linked)
            .linked_input(balance_linked)
            .build()?;

        Ok(Transfer {
            proof,
            sender: self.name.clone(),
            receiver,
            encrypted_amount_sender,
            encrypted_amount_receiver,
        })
    }

    /// Create a public deposit to a private balance.
    pub fn create_deposit(&self, amount: i64) -> Deposit {
        Deposit {
            public_amount: amount,
            name: self.name.clone(),
        }
    }

    /// Create a refresh balance transaction.
    pub fn create_refresh_balance(&self, chain: &Chain) -> Result<RefreshBalance> {
        let mut builder = self.runtime.linkedproof_builder();

        // Decrypt current balance, returning a link to the underlying message
        let balance_encrypted = chain.balances.get(&self.name).unwrap();
        let (balance, existing_link) =
            builder.decrypt_returning_link::<Signed>(balance_encrypted, &self.private_key)?;

        // Re-encrypt the current balance, returning a link to the underlying message
        println!("    {}: re-encrypting balance of {}", self.name, balance);
        let (fresh_balance, fresh_link) =
            builder.encrypt_returning_link(&balance, &self.public_key)?;

        // Generate proof that the ciphertexts encrypt the same underlying message and that
        // the new one has a fresh noise budget and fresh encoding.
        println!("    {}: creating refresh balance linkedproof", self.name);
        let proof = builder
            .zkp_program(self.app.get_refresh_balance_zkp())?
            .linked_input(existing_link)
            .linked_input(fresh_link)
            .build()?;

        Ok(RefreshBalance {
            proof,
            fresh_balance,
            name: self.name.clone(),
        })
    }

    /// Create a register transaction.
    pub fn create_register(&self, initial_deposit: i64) -> Result<Register> {
        let mut builder = self.runtime.linkedproof_builder();

        // Encrypt deposit amount
        println!(
            "    {}: encrypting and linking {}",
            self.name, initial_deposit
        );
        let (amount_enc, amount_linked) =
            builder.encrypt_returning_link(&Signed::from(initial_deposit), &self.public_key)?;

        // Create registration proof
        println!("    {}: creating registration linkedproof", self.name);
        let proof = builder
            .zkp_program(self.app.get_registration_zkp())?
            .linked_input(amount_linked)
            .public_input(BulletproofsField::from(initial_deposit))
            .build()?;

        Ok(Register {
            proof,
            encrypted_amount: amount_enc,
            public_key: self.public_key.clone(),
            deposit: self.create_deposit(initial_deposit),
        })
    }
}

/// A register transaction.
///
/// The SDLP in the linked proof proves that the ciphertext is a valid, fresh encryption. The R1CS
/// ZKP in the linked proof proves that the amount encrypted matches the public amount deposited.
#[derive(Clone)]
pub struct Register {
    proof: LinkedProof,
    public_key: PublicKey,
    encrypted_amount: Ciphertext,
    deposit: Deposit,
}

/// A public deposit transaction.
#[derive(Clone)]
pub struct Deposit {
    public_amount: i64,
    name: Username,
}

/// A private transfer transaction.
///
/// The SDLP in the linked proof proves that the ciphertexts are valid, fresh encryptions of the
/// same value. The R1CS ZKP in the linked proof proves that the amount encrypted does not exceed
/// the sender's current balance.
#[derive(Clone)]
pub struct Transfer {
    proof: LinkedProof,
    // Transfer amount encrypted under sender's key
    encrypted_amount_sender: Ciphertext,
    // Transfer amount encrypted under receiver's key
    encrypted_amount_receiver: Ciphertext,
    sender: Username,
    receiver: Username,
}

/// A refresh private balance transaction.
///
/// The SDLP in the linked proof proves that the fresh balance is a valid, fresh encryption (to
/// avoid overflowing the noise budget). The R1CS ZKP in the linked proof proves that the new
/// encryption is also _freshly encoded_ (to avoid overflowing the plaintext modulus) and that it
/// matches the existing value on chain.
#[derive(Clone)]
pub struct RefreshBalance {
    proof: LinkedProof,
    fresh_balance: Ciphertext,
    name: Username,
}

/// A chain transaction.
pub enum Transaction {
    Register(Register),
    Deposit(Deposit),
    Transfer(Transfer),
    RefreshBalance(RefreshBalance),
}

/// Perspective of the blockchain, basically just a place where user's encrypted balances are
/// stored and transparent FHE computations take place in the form of atomic transactions.
///
/// In this simple example, assume read-only references `&Chain` provide "call" functionalities,
/// i.e. non-mutating methods for reading chain data, and mutable references `&mut Chain` provide
/// "send" functionalities, i.e. sending transactions that can mutate chain data.
pub struct Chain {
    /// The current balances
    balances: HashMap<Username, Ciphertext>,
    /// The user's public keys
    keys: HashMap<Username, PublicKey>,
    /// Ledger of transactions
    ledger: Vec<Transaction>,
    /// App holding FHE and ZKP programs
    app: App,
    /// Runtime to run FHE programs and verify proofs
    runtime: FheZkpRuntime<BulletproofsBackend>,
}

impl Chain {
    pub fn new() -> Result<Self> {
        let app = App::new()?;
        let runtime = FheZkpRuntime::new(app.params(), &BulletproofsBackend::new())?;
        Ok(Self {
            balances: HashMap::new(),
            keys: HashMap::new(),
            ledger: Vec::new(),
            runtime,
            app,
        })
    }

    pub fn register(&mut self, register: Register) -> Result<()> {
        self.ledger.push(Transaction::Register(register.clone()));
        let Register {
            proof,
            encrypted_amount,
            public_key,
            deposit,
        } = register;

        // First, verify that the encrypted amount matches the public amount
        let mut builder = self.runtime.linkedproof_verification_builder();
        builder.encrypt_returning_link::<Signed>(&encrypted_amount, &public_key)?;
        builder
            .zkp_program(self.app.get_registration_zkp())?
            .proof(proof)
            .public_input(BulletproofsField::from(deposit.public_amount))
            .verify()?;

        // Register the user's public key
        self.keys.insert(deposit.name.clone(), public_key);

        // Set the initial encrypted balance
        self.balances.insert(deposit.name, encrypted_amount);
        Ok(())
    }

    pub fn deposit(&mut self, deposit: Deposit) -> Result<()> {
        self.ledger.push(Transaction::Deposit(deposit.clone()));
        let Deposit {
            public_amount,
            name,
        } = deposit;

        // Deposit into the user's balance
        let pk = self.keys.get(&name).unwrap();
        let curr_bal = self.balances.get_mut(&name).unwrap();
        *curr_bal = self
            .runtime
            .run::<FheProgramInput>(
                self.app.get_deposit_to_fhe(),
                vec![curr_bal.clone().into(), Signed::from(public_amount).into()],
                pk,
            )?
            .remove(0);
        Ok(())
    }

    pub fn transfer(&mut self, transfer: Transfer) -> Result<()> {
        self.ledger.push(Transaction::Transfer(transfer.clone()));
        let Transfer {
            proof,
            encrypted_amount_sender,
            encrypted_amount_receiver,
            sender,
            receiver,
        } = transfer;

        // First verify the transfer is valid
        let mut builder = self.runtime.linkedproof_verification_builder();
        let link = builder.encrypt_returning_link::<Signed>(
            &encrypted_amount_sender,
            self.keys.get(&sender).unwrap(),
        )?;
        builder.reencrypt(
            &link,
            &encrypted_amount_receiver,
            self.keys.get(&receiver).unwrap(),
        )?;
        builder.decrypt_returning_link::<Signed>(self.balances.get(&sender).unwrap())?;
        builder
            .zkp_program(self.app.get_transfer_zkp())?
            .proof(proof)
            .verify()?;

        // Update the sender's balance:
        let sender_pk = self.keys.get(&sender).unwrap();
        let sender_balance = self.balances.get_mut(&sender).unwrap();
        *sender_balance = self
            .runtime
            .run(
                self.app.get_transfer_from_fhe(),
                vec![sender_balance.clone(), encrypted_amount_sender],
                sender_pk,
            )?
            .remove(0);

        // Update receiver's balance
        let receiver_pk = self.keys.get(&receiver).unwrap();
        let receiver_balance = self.balances.get_mut(&receiver).unwrap();
        *receiver_balance = self
            .runtime
            .run(
                self.app.get_transfer_to_fhe(),
                vec![receiver_balance.clone(), encrypted_amount_receiver],
                receiver_pk,
            )?
            .remove(0);
        Ok(())
    }

    pub fn refresh_balance(&mut self, refresh_balance: RefreshBalance) -> Result<()> {
        self.ledger
            .push(Transaction::RefreshBalance(refresh_balance.clone()));
        let RefreshBalance {
            proof,
            fresh_balance,
            name,
        } = refresh_balance;

        // Verify the balance refresh is valid
        let mut builder = self.runtime.linkedproof_verification_builder();
        builder.decrypt_returning_link::<Signed>(self.balances.get(&name).unwrap())?;
        builder.encrypt_returning_link::<Signed>(&fresh_balance, self.keys.get(&name).unwrap())?;
        builder
            .zkp_program(self.app.get_refresh_balance_zkp())?
            .proof(proof)
            .verify()?;

        // Use the freshly encrypted balance
        self.balances
            .insert(name, fresh_balance)
            .expect("User should be registered");
        Ok(())
    }

    pub fn print_ledger(&self) {
        for (i, tx) in self.ledger.iter().enumerate() {
            match tx {
                Transaction::Register(r) => println!("{i}. User {} registered", r.deposit.name,),
                Transaction::Deposit(d) => {
                    println!("{i}. User {} deposited {}", d.name, d.public_amount)
                }
                Transaction::Transfer(t) => println!(
                    "{i}. User {} transferred <ENCRYPTED> to {}",
                    t.sender, t.receiver
                ),
                Transaction::RefreshBalance(b) => {
                    println!("{i}. User {} refreshed their balance", b.name)
                }
            }
        }
    }
}

pub struct App(FheZkpApplication);

impl App {
    pub fn new() -> Result<Self> {
        let app = Compiler::new()
            .fhe_program(transfer_to)
            .fhe_program(transfer_from)
            .fhe_program(deposit_to)
            // These params are not necessary to run the example, but they do shave a few
            // minutes off the runtime. In practice, you probably want to use the default
            // parameters provided by the compiler. The ones set here will result in balances
            // needing to be refreshed more often.
            .with_params(&Params {
                lattice_dimension: 1024,
                coeff_modulus: vec![0x7e00001],
                plain_modulus: 512,
                scheme_type: sunscreen::SchemeType::Bfv,
                security_level: sunscreen::SecurityLevel::TC128,
            })
            .zkp_backend::<BulletproofsBackend>()
            .zkp_program(validate_transfer)
            .zkp_program(validate_registration)
            .zkp_program(validate_refresh_balance)
            .compile()?;
        Ok(Self(app))
    }

    pub fn get_transfer_zkp(&self) -> &CompiledZkpProgram {
        self.0.get_zkp_program(validate_transfer).unwrap()
    }

    pub fn get_registration_zkp(&self) -> &CompiledZkpProgram {
        self.0.get_zkp_program(validate_registration).unwrap()
    }

    pub fn get_refresh_balance_zkp(&self) -> &CompiledZkpProgram {
        self.0.get_zkp_program(validate_refresh_balance).unwrap()
    }

    pub fn get_transfer_to_fhe(&self) -> &CompiledFheProgram {
        self.0.get_fhe_program(transfer_to).unwrap()
    }

    pub fn get_transfer_from_fhe(&self) -> &CompiledFheProgram {
        self.0.get_fhe_program(transfer_from).unwrap()
    }

    pub fn get_deposit_to_fhe(&self) -> &CompiledFheProgram {
        self.0.get_fhe_program(deposit_to).unwrap()
    }

    pub fn params(&self) -> &Params {
        self.0.params()
    }
}

fn main() -> Result<()> {
    println!("Starting a new chain...");
    let mut chain = Chain::new()?;

    println!();

    println!("Running Alice's transactions...");
    let alice = User::new("Alice")?;
    let deposit = 100;
    println!("Registering with a deposit of {deposit}");
    chain.register(alice.create_register(deposit)?)?;
    let deposit = 50;
    println!("Depositing an extra {deposit}");
    chain.deposit(alice.create_deposit(deposit))?;

    println!();

    println!("Running Bob's transactions...");
    let bob = User::new("Bob")?;
    let deposit = 100;
    println!("Registering with a deposit of {deposit}");
    chain.register(bob.create_register(deposit)?)?;
    let tx = 50;
    println!("Transfering {tx} to Alice");
    chain.transfer(bob.create_transfer(&chain, tx, "Alice")?)?;

    println!();

    println!("Refreshing Alice's balance...");
    let refresh_balance = alice.create_refresh_balance(&chain)?;
    chain.refresh_balance(refresh_balance)?;

    println!("Done!");

    println!();
    println!("========================== Ledger ==========================");
    println!();
    chain.print_ledger();

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn main_works() -> Result<()> {
        main()
    }
}
1

It's worth noting that we are also implicitly relying on the fact that FHE programs are deterministic. You could imagine a scenario where one tries to accomplish this by having a trusted party decrypt the inputs, perform the computation, and then encrypt the result - but that encryption of the result relies on randomness, which is generally not available for a consensus-driven compute setting like a blockchain. Because FHE programs are deterministic, any validators running the computation will always get the exact same ciphertext result, allowing consensus to proceed.

2

In practice, you may wish to restrict the ciphertexts of transaction or deposit amounts to also be fresh encodings. With some additional metadata on chain indicating how many modifications have been performed on a user's balance, you could effectively track how close the coefficients are to the plaintext modulus, and then restrict a user from making transactions unless they refresh their balance.