diff --git a/sunscreen_docs/book.toml b/sunscreen_docs/book.toml index 0dc0ffb4f..8fdf7ac29 100644 --- a/sunscreen_docs/book.toml +++ b/sunscreen_docs/book.toml @@ -1,5 +1,5 @@ [book] -authors = ["Rick Weber", "Ravital Solomon", "Sam Tay"] +authors = ["Rick Weber", "Ravital Solomon", "Sam Tay", "Ryan Orendorff"] language = "en" multilingual = false src = "src" diff --git a/sunscreen_docs/src/SUMMARY.md b/sunscreen_docs/src/SUMMARY.md index 565137e47..7c72cc563 100644 --- a/sunscreen_docs/src/SUMMARY.md +++ b/sunscreen_docs/src/SUMMARY.md @@ -15,6 +15,7 @@ - [What's in an FHE program?](fhe/fhe_programs/fhe_programs.md) - [Types](fhe/fhe_programs/types/types.md) - [Signed](fhe/fhe_programs/types/signed.md) + - [Unsigned](fhe/fhe_programs/types/unsigned.md) - [Fractional](fhe/fhe_programs/types/fractional.md) - [Rational](fhe/fhe_programs/types/rational.md) - [How to write an FHE program](fhe/fhe_programs/writing_an_fhe_program/writing_an_fhe_program.md) @@ -77,3 +78,23 @@ - [Constant inputs](zkp/advanced/constant_inputs.md) - [Creating ZKP types](zkp/advanced/zkp_type.md) - [WASM support](zkp/advanced/wasm.md) + +# FHE + ZKP + +- [Introduction](linked/intro/intro.md) + - [How does this work?](linked/intro/how.md) +- [What's in a Linked ZKP program?](linked/linked_programs/linked_programs.md) + - [Types](linked/linked_programs/types.md) + - [Limitations](linked/linked_programs/limitations.md) +- [Compiling](linked/compiling/compiling.md) +- [Runtime](linked/runtime/runtime.md) + - [Proving](linked/runtime/prove.md) + - [Verifying](linked/runtime/verify.md) + - [Serialization](linked/runtime/serialization.md) +- [Applications](linked/applications/applications.md) + - [Private transactions](linked/applications/private_tx.md) +- [FAQ](linked/faq/faq.md) +- [Advanced topics](linked/advanced/advanced.md) + - [Plaintext modulus](linked/advanced/plain_modulus.md) + - [Custom bounds](linked/advanced/custom_bounds.md) + - [Short discrete log proof](linked/advanced/sdlp.md) diff --git a/sunscreen_docs/src/fhe/fhe_programs/types/unsigned.md b/sunscreen_docs/src/fhe/fhe_programs/types/unsigned.md new file mode 100644 index 000000000..cbadc319b --- /dev/null +++ b/sunscreen_docs/src/fhe/fhe_programs/types/unsigned.md @@ -0,0 +1,19 @@ +# Unsigned + +Our unsigned types actually come in a few different flavors, depending on the +number of bits you need. Just like the [`crypto_bigint::Uint`](https://docs.rs/crypto-bigint/latest/crypto_bigint/struct.Uint.html) type, you can specify however many word-sized limbs you need for your computation: + +```rust,ignore +struct Unsigned; +``` + +and we provide a few type synonyms for common bit sizes (`Unsigned64`, +`Unsigned128`, `Unsigned256`, and `Unsigned512`). + +These unsigned types allow you to perform integer arithmetic as follows (recall that at least one operand must be a ciphertext): + +operation | operand +----------|------------------------------------------------------------ +add | ciphertext, plaintext, `Uint` literal, `u64` literal +sub | ciphertext, plaintext, `Uint` literal, `u64` literal +mul | ciphertext, plaintext, `Uint` literal, `u64` literal diff --git a/sunscreen_docs/src/linked/advanced/advanced.md b/sunscreen_docs/src/linked/advanced/advanced.md new file mode 100644 index 000000000..cdb978e1a --- /dev/null +++ b/sunscreen_docs/src/linked/advanced/advanced.md @@ -0,0 +1,3 @@ +# Advanced topics + +Now that you've gotten the basics down, let's dive into some more complex topics. diff --git a/sunscreen_docs/src/linked/advanced/custom_bounds.md b/sunscreen_docs/src/linked/advanced/custom_bounds.md new file mode 100644 index 000000000..25c9ee00b --- /dev/null +++ b/sunscreen_docs/src/linked/advanced/custom_bounds.md @@ -0,0 +1,59 @@ +# Custom bounds + +If you are comfortable with the math behind the [SDLP](/linked/intro.how.md), +advanced users may wish to customize certain bounds in the secret `S`. Note that +the correctness of linking types relies on the bounds we've used in our +implementation (which varies among the FHE types but generally looks like a +bound up to the plaintext modulus for coefficients under degree 256, and zero +for greater coefficients). For this reason, we expressly discourage changing the +bounds for any messages that are linked to ZKP programs. However, you may wish +to change the bound on noise terms for computed ciphertexts; we use a liberal +bound of $\Delta/2$ for each coefficient in the noise polynomial, which is the +maximum noise permitted for a valid decryption. If you want to ensure that a +computed ciphertext has much less noise, perhaps to use it as an input for +further computation, you can lower this bound. + +To do this, first familiarize yourself with the [documentation](https://docs.rs/logproof/latest/logproof/bfv_statement/fn.generate_prover_knowledge.html) concerning the shape of `S`. +Then you can modify its bounds with the code below. + + +```rust,no_run +{{#rustdoc_include ../basic_prog.rs:none}} +use sunscreen::linked::Bounds; + +# fn main() -> Result<(), Error> { +let app = Compiler::new() + .fhe_program(increase_by_factor) + .zkp_backend::() + .zkp_program(is_greater_than_one) + .compile()?; +let runtime = FheZkpRuntime::new(app.params(), &BulletproofsBackend::new())?; +let (public_key, private_key) = runtime.generate_keys()?; +# let existing_ct = runtime.encrypt(Signed::from(2), &public_key)?; + +let mut proof_builder = runtime.linkedproof_builder(); + +// Assume existing ciphertext comes out of a computation +let (pt, link) = proof_builder.decrypt_returning_link::(&existing_ct, &private_key)?; + +// For a single decryption statement, S will have one column and four rows, with +// the last entry containing the noise. Let's lower the bound on each +// coefficient in the noise polynomial to 32 bits. +let degree = app.params().lattice_dimension as usize; +let proof = proof_builder + .add_custom_bounds(3, 0, Bounds(vec![32; degree])) + .zkp_program(app.get_zkp_program(is_greater_than_one).unwrap())? + .linked_input(link) + .build()?; + +let mut verify_builder = runtime.linkedproof_verification_builder(); +verify_builder.decrypt_returning_link::(&existing_ct)?; +// The verifier must specify the same bounds! +verify_builder + .add_custom_bounds(3, 0, Bounds(vec![32; degree])) + .proof(proof) + .zkp_program(app.get_zkp_program(is_greater_than_one).unwrap())? + .verify()?; +# Ok(()) +# } +``` diff --git a/sunscreen_docs/src/linked/advanced/plain_modulus.md b/sunscreen_docs/src/linked/advanced/plain_modulus.md new file mode 100644 index 000000000..7f1cf7942 --- /dev/null +++ b/sunscreen_docs/src/linked/advanced/plain_modulus.md @@ -0,0 +1,15 @@ +# Plaintext modulus + +First, make sure you've read through the [previous chapter describing the +plaintext modulus](/fhe/advanced/plain_modulus/plain_modulus.md). We mentioned +that decreasing our default plaintext modulus can increase performance in FHE +programs, and the same is true of linked ZKP programs. In fact, the size of the +linked proof will also decrease with a lower plaintext modulus. If you are an +advanced user looking to tune the proof size and prover/verifier times, you +ought to consider whether or not your application can support a lower plaintext +modulus. + +However, our current implementation requires that the plaintext modulus be a +power of 2. If you specify a modulus that is not a power of two, your linked ZKP +program will fail to compile. We may offer support for other plaintext modulus +values in the future — reach out if you have a use case in mind! diff --git a/sunscreen_docs/src/linked/advanced/sdlp.md b/sunscreen_docs/src/linked/advanced/sdlp.md new file mode 100644 index 000000000..ef347ce80 --- /dev/null +++ b/sunscreen_docs/src/linked/advanced/sdlp.md @@ -0,0 +1,27 @@ +# Short discrete log proof + +If you only have to prove that ciphertexts are well formed and within certain +noise bounds, and you don't have any arbitrary properties to prove about the +encrypted values, you can also use an `Sdlp` on its own, rather than a full +`LinkedProof` and ZKP program. + +```rust +{{#rustdoc_include ../basic_prog.rs:none}} +# fn main() -> Result<(), Error> { +let app = Compiler::new() + .fhe_program(increase_by_factor) + .compile()?; +let runtime = FheRuntime::new(app.params())?; +let (public_key, private_key) = runtime.generate_keys()?; + +let mut proof_builder = runtime.sdlp_builder(); +let ct = proof_builder.encrypt(&Signed::from(2), &public_key)?; +let proof = proof_builder.build()?; + +let mut verify_builder = runtime.sdlp_verification_builder(); +verify_builder.encrypt(&ct, &public_key)?; +verify_builder.proof(proof).verify()?; + +# Ok(()) +# } +``` diff --git a/sunscreen_docs/src/linked/applications/applications.md b/sunscreen_docs/src/linked/applications/applications.md new file mode 100644 index 000000000..e0d47fa00 --- /dev/null +++ b/sunscreen_docs/src/linked/applications/applications.md @@ -0,0 +1,4 @@ +# Applications + +In this section, we'll take a look at a robust, private, and trustless system for private +transactions in an environment with transparent computation. diff --git a/sunscreen_docs/src/linked/applications/private_tx.md b/sunscreen_docs/src/linked/applications/private_tx.md new file mode 100644 index 000000000..c6c0e0c28 --- /dev/null +++ b/sunscreen_docs/src/linked/applications/private_tx.md @@ -0,0 +1,292 @@ +# 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](https://github.com/Sunscreen-tech/Sunscreen/blob/main/examples/private_tx_linkedproof/src/main.rs) if you want to see it altogether. + +### Setup + +First, let's import everything we need: + +```rust,ignore +{{#include private_tx.rs:imports}} +``` + +### 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. + +```rust,ignore +{{#include private_tx.rs:fhe_programs}} +``` + +### 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](/fhe/advanced/noise_margin.md) that causes the receiver's balance to be +un-decryptable. + +As we'll see [below](#transfer-2), the last two properties are handled outside of the ZKP +program (by the [SDLP](/linked/intro/how.md)), so let's validate the first two +properties. + +```rust,ignore +{{#include private_tx.rs:validate_transfer}} +``` + +#### 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. + +```rust,ignore +{{#include private_tx.rs:validate_registration}} +``` + +#### 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](/fhe/advanced/noise_margin.md) +2. the encrypted plaintext doesn't overflow its [plaintext modulus](/fhe/advanced/plain_modulus/plain_modulus.md) + +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] + +```rust,ignore +{{#include private_tx.rs:validate_refresh_balance}} +``` + +### 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. + +```rust,ignore +{{#include private_tx.rs:app_1}} +{{#include private_tx.rs:app_2}} +``` + +### 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. + +```rust,ignore +{{#include private_tx.rs:username_type}} + +{{#include private_tx.rs:transfer_type}} +``` + +#### 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. + +```rust,ignore +{{#include private_tx.rs:deposit_type}} +``` + +#### 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. + +```rust,ignore +{{#include private_tx.rs:register_type}} +``` + +#### Refresh balance + +Lastly, refreshing a balance requires the new ciphertext and its accompanying +proof of validity. + +```rust,ignore +{{#include private_tx.rs:refresh_type}} +``` + +### 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. + +```rust,ignore +{{#include private_tx.rs:user_type}} + +impl User { +{{#include private_tx.rs:user_new}} +} + +{{#include private_tx.rs:transaction_type}} + +{{#include private_tx.rs:chain_type}} + +impl Chain { +{{#include private_tx.rs:chain_new}} +} +``` + +### 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. + +```rust,ignore +impl User { +{{#include private_tx.rs:user_deposit}} + +{{#include private_tx.rs:user_register}} +} +``` + +#### 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. + +```rust,ignore +impl User { +{{#include private_tx.rs:user_transfer}} +} +``` + +#### 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. + +```rust,ignore +impl User { +{{#include private_tx.rs:user_refresh}} +} +``` + +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. + +```rust,ignore +impl Chain { +{{#include private_tx.rs:chain_register}} +} +``` + +#### 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. + +```rust,ignore +impl Chain { +{{#include private_tx.rs:chain_deposit}} +} +``` + +#### 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. + +```rust,ignore +impl Chain { +{{#include private_tx.rs:chain_transfer}} +} +``` + +#### Refresh balance + +Finally, to refresh a balance the chain simply needs to verify the proof and +then overwrite the existing balance. + +```rust,ignore +impl Chain { +{{#include private_tx.rs:chain_refresh}} +} +``` + + +### Run it! + +Finally, here's a runnable `main` function demonstrating the transactions above: + +```rust +{{#rustdoc_include private_tx.rs: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](/linked/advanced/plain_modulus.md), and then restrict a user from making transactions unless they refresh their balance. diff --git a/sunscreen_docs/src/linked/applications/private_tx.rs b/sunscreen_docs/src/linked/applications/private_tx.rs new file mode 100644 index 000000000..b1f54bdf3 --- /dev/null +++ b/sunscreen_docs/src/linked/applications/private_tx.rs @@ -0,0 +1,613 @@ +// ANCHOR: imports +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, +}; +// ANCHOR_END: imports + +// ANCHOR: fhe_programs +/// Subtract the transaction amount from the sender's balance. +#[fhe_program(scheme = "bfv")] +fn transfer_from(balance: Cipher, tx: Cipher) -> Cipher { + balance - tx +} + +/// Add the transaction amount to the receiver's balance. +#[fhe_program(scheme = "bfv")] +fn transfer_to(balance: Cipher, tx: Cipher) -> Cipher { + balance + tx +} + +/// Add the public transaction amount to a user's balance. +#[fhe_program(scheme = "bfv")] +fn deposit_to(balance: Cipher, deposit: Signed) -> Cipher { + balance + deposit +} +// ANCHOR_END: fhe_programs + +// ANCHOR: zkp_programs +// ANCHOR: validate_transfer +/// Validate a transfer transaction. +#[zkp_program] +fn validate_transfer( + #[linked] tx: BfvSigned, + #[linked] sender_balance: BfvSigned, +) { + 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); +} +// ANCHOR_END: validate_transfer + +// ANCHOR: validate_registration +/// Validate registration. The deposit amount is public, but we must prove that the provided +/// ciphertext encrypts the deposit amount. +#[zkp_program] +fn validate_registration( + #[linked] encrypted_deposit: BfvSigned, + #[public] public_deposit: Field, +) { + let encrypted_deposit = encrypted_deposit.into_field_elem(); + encrypted_deposit.constrain_eq(public_deposit); +} +// ANCHOR_END: validate_registration + +// ANCHOR: validate_refresh_balance +/// 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( + #[linked] existing_balance: BfvSigned, + #[linked] fresh_balance: BfvSigned, +) { + fresh_balance.constrain_fresh_encoding(); + fresh_balance + .into_field_elem() + .constrain_eq(existing_balance.into_field_elem()); +} +// ANCHOR_END: validate_refresh_balance +// ANCHOR_END: zkp_programs + +// ANCHOR: username_type +/// A way to identify a user. +type Username = String; +// ANCHOR_END: username_type + +// ANCHOR: user_type +/// 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, +} +// ANCHOR_END: user_type + +impl User { +// ANCHOR: user_new + pub fn new(name: &str) -> Result { + 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, + }) + } +// ANCHOR_END: user_new + +// ANCHOR: user_transfer + /// Create a private, validated transfer to send to another user. + pub fn create_transfer>( + &self, + chain: &Chain, + amount: i64, + receiver: U, + ) -> Result { + 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::(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, + }) + } +// ANCHOR_END: user_transfer + +// ANCHOR: user_deposit + /// Create a public deposit to a private balance. + pub fn create_deposit(&self, amount: i64) -> Deposit { + Deposit { + public_amount: amount, + name: self.name.clone(), + } + } +// ANCHOR_END: user_deposit + +// ANCHOR: user_refresh + /// Create a refresh balance transaction. + pub fn create_refresh_balance(&self, chain: &Chain) -> Result { + 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::(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(), + }) + } +// ANCHOR_END: user_refresh + +// ANCHOR: user_register + /// Create a register transaction. + pub fn create_register(&self, initial_deposit: i64) -> Result { + 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), + }) + } +// ANCHOR_END: user_register +} + +// ANCHOR: register_type +/// 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, +} +// ANCHOR_END: register_type + +// ANCHOR: deposit_type +/// A public deposit transaction. +#[derive(Clone)] +pub struct Deposit { + public_amount: i64, + name: Username, +} +// ANCHOR_END: deposit_type + +// ANCHOR: transfer_type +/// 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, +} +// ANCHOR_END: transfer_type + +// ANCHOR: refresh_type +/// 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, +} +// ANCHOR_END: refresh_type + +// ANCHOR: transaction_type +/// A chain transaction. +pub enum Transaction { + Register(Register), + Deposit(Deposit), + Transfer(Transfer), + RefreshBalance(RefreshBalance), +} +// ANCHOR_END: transaction_type + +// ANCHOR: chain_type +/// 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, + /// The user's public keys + keys: HashMap, + /// Ledger of transactions + ledger: Vec, + /// App holding FHE and ZKP programs + app: App, + /// Runtime to run FHE programs and verify proofs + runtime: FheZkpRuntime, +} +// ANCHOR_END: chain_type + +impl Chain { +// ANCHOR: chain_new + pub fn new() -> Result { + 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, + }) + } +// ANCHOR_END: chain_new + +// ANCHOR: chain_register + 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::(&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(()) + } +// ANCHOR_END: chain_register + +// ANCHOR: chain_deposit + 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::( + self.app.get_deposit_to_fhe(), + vec![curr_bal.clone().into(), Signed::from(public_amount).into()], + pk, + )? + .remove(0); + Ok(()) + } +// ANCHOR_END: chain_deposit + +// ANCHOR: chain_transfer + 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::( + &encrypted_amount_sender, + self.keys.get(&sender).unwrap(), + )?; + builder.reencrypt( + &link, + &encrypted_amount_receiver, + self.keys.get(&receiver).unwrap(), + )?; + builder.decrypt_returning_link::(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(()) + } +// ANCHOR_END: chain_transfer + +// ANCHOR: chain_refresh + 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::(self.balances.get(&name).unwrap())?; + builder.encrypt_returning_link::(&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(()) + } +// ANCHOR_END: chain_refresh + + 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 to {}", + t.sender, t.receiver + ), + Transaction::RefreshBalance(b) => { + println!("{i}. User {} refreshed their balance", b.name) + } + } + } + } +} + +// ANCHOR: app_1 +pub struct App(FheZkpApplication); + +impl App { + pub fn new() -> Result { + let app = Compiler::new() + .fhe_program(transfer_to) + .fhe_program(transfer_from) + .fhe_program(deposit_to) +// ANCHOR_END: app_1 + // 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, + }) +// ANCHOR: app_2 + .zkp_backend::() + .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() + } +} +// ANCHOR_END: app_2 + +// ANCHOR: main +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(()) +} +// ANCHOR_END: main + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn main_works() -> Result<()> { + main() + } +} diff --git a/sunscreen_docs/src/linked/basic_prog.rs b/sunscreen_docs/src/linked/basic_prog.rs new file mode 100644 index 000000000..0b3ad7e96 --- /dev/null +++ b/sunscreen_docs/src/linked/basic_prog.rs @@ -0,0 +1,40 @@ +// ANCHOR: all +// ANCHOR: imports +use sunscreen::{ + bulletproofs::BulletproofsBackend, + fhe_program, + linked::{LinkedProof, LinkedProofBuilder}, + types::{ + bfv::Signed, + zkp::{ + AsFieldElement, BfvSigned, BulletproofsField, ConstrainCmp, ConstrainFresh, Field, + FieldSpec, + }, + Cipher, + }, + zkp_program, zkp_var, Ciphertext, CompiledFheProgram, CompiledZkpProgram, Compiler, Error, + FheProgramInput, FheRuntime, FheZkpApplication, FheZkpRuntime, Params, PrivateKey, PublicKey, + Result, ZkpProgramInput, +}; +// ANCHOR_END: imports + +// ANCHOR: progs +// ANCHOR: fhe_prog +#[fhe_program(scheme = "bfv")] +fn increase_by_factor(x: Signed, scale: Cipher) -> Cipher { + x * scale +} +// ANCHOR_END: fhe_prog + +// ANCHOR: zkp_prog +#[zkp_program] +fn is_greater_than_one(#[linked] scale: BfvSigned) { + scale + .into_field_elem() + .constrain_gt_bounded(zkp_var!(1), 64); +} +// ANCHOR_END: zkp_prog +// ANCHOR_END: progs +// ANCHOR: none +// ANCHOR_END: none +// ANCHOR_END: all diff --git a/sunscreen_docs/src/linked/compiling/compiling.md b/sunscreen_docs/src/linked/compiling/compiling.md new file mode 100644 index 000000000..cd4748e2a --- /dev/null +++ b/sunscreen_docs/src/linked/compiling/compiling.md @@ -0,0 +1,40 @@ +# Compiling + +Compiling a ZKP program with linked inputs differs slightly from [compiling a normal ZKP program](/zkp/compiling/compiling.md). This is because the inputs that get linked depend on the FHE parameters used in their creation. For this reason, it's not possible to just call `zkp_progam.compile()?`, because there's not enough context to know what these FHE parameters are. + +Instead, you'll need to invoke a full `Compiler` and specify an `fhe_program` so +that we know what FHE parameters to use when compiling the `zkp_program`. Don't +worry — our types are defined so that you won't even be able to specify the +linked ZKP program unless you've already passed an `fhe_program` — doing +otherwise will result in a Rust compile-time error. + +This will not compile: +```rust,no_run,compile_fail +{{#rustdoc_include ../basic_prog.rs:zkp_prog}} + +# fn main() -> Result<(), Error> { +let app = Compiler::new() + .zkp_backend::() + .zkp_program(is_greater_than_one) // This is a (rust) compile-time error! + .compile()?; +# Ok(()) +# } +``` + +but this will: + +```rust +{{#rustdoc_include ../basic_prog.rs:progs}} + +# fn main() -> Result<(), Error> { +let app = Compiler::new() + .fhe_program(increase_by_factor) + .zkp_backend::() + .zkp_program(is_greater_than_one) + .compile()?; +# Ok(()) +# } +``` + +If you already have FHE parameters for your application, you can specify them +directly using the method [`Compiler::with_params`](https://docs.rs/sunscreen/latest/sunscreen/struct.GenericCompiler.html#method.with_params). diff --git a/sunscreen_docs/src/linked/faq/faq.md b/sunscreen_docs/src/linked/faq/faq.md new file mode 100644 index 000000000..edef1480b --- /dev/null +++ b/sunscreen_docs/src/linked/faq/faq.md @@ -0,0 +1,4 @@ +# FAQ + +> ST: Unclear what FAQs there might be, but I'm guessing someone will think of +some, so leaving this stub here. diff --git a/sunscreen_docs/src/linked/intro/how.md b/sunscreen_docs/src/linked/intro/how.md new file mode 100644 index 000000000..7be7ee7b3 --- /dev/null +++ b/sunscreen_docs/src/linked/intro/how.md @@ -0,0 +1,28 @@ +# How does this work? + +This chapter is not a prerequisite to using our linked compilers, but may be of +interest for anyone curious what's going on under the hood. + +# Linked SDLP and R1CS proofs + +A linked proof consists of a short discrete log proof (SDLP) and an R1CS bulletproof (BP). It allows you to simultaneously prove an encryption is valid (SDLP) and that the encrypted message has some property (BP). Specifically, the SDLP proves a linear relation while keeping part of that relation secret, while BPs enables proving arbitrary arithmetic circuits, which can be used to prove that a secret satisfies some property. For example, one can prove that a private transaction can occur because the sender has enough funds to cover the transaction, without revealing what the transaction is or what their current balance is. This combination of proof systems is powerful because we can now operate on encrypted data using FHE while ensuring the encrypted data doesn't violate any properties of the system of interest such as a negative balance. + +How does this work in practice? The sunscreen library provides a [`LinkedProofBuilder`](/linked/runtime/prove.md) that allows you to encrypt messages in a very similar way to our typical [`FheRuntime::encrypt`](/fhe/fhe_programs/runtime/encryption.md), while also opting to _link_ a message as an input to a ZKP program. Under the hood, we'll handle the complicated bits of generating the SDLP and sharing the secrets with the ZKP program. + +# The nitty gritty + +The SDLP proves linear equations of the form \\( A \cdot S = T \\), where \\(A\\) and \\(T\\) are public information, while \\(S\\) is only known by the prover. The BFV equations can be written in this linear form, where the message and associated randomness from encryption can be contained in the private \\(S\\) matrix. + +In order to link this to BPs, we pass the values in \\(S\\) that the user would like to link with BPs as inputs to the BPs circuit. If one just does this without any other modifications, the result is not secure as there is not a guarantee that the inputs to the SDLP were the same as the inputs to the BP. To rectify this, we form commitments to the parts of \\(S\\) that are shared between SDLP and BPs and check that the inputs that are linked between the two proof systems produce the same commitment. + +Written out in steps, we perform the following as a prover: + +1. Generate a SDLP that the user requested. The parts of \\(S\\) that the user would like to link between SDLP and BPs are committed to, and these values and the associated generators are stored by the prover. +2. The prover passes the inputs and generators to BPs. +3. The SDLP and BP proofs are stored together, along with the commitment to the linked inputs. + +A verifier will then perform the following steps: + +1. The verifier will run the SDLP verification with the public inputs \\(A\\) and \\(T\\) and verify the result. As part of this process, a commitment to the linked inputs is derived. +2. The verifier will run the BP verification with its public inputs and verify the result. As part of this process, a commitment to the linked inputs is derived. +3. The verifier checks that the commitments generated in the prior steps match. diff --git a/sunscreen_docs/src/linked/intro/intro.md b/sunscreen_docs/src/linked/intro/intro.md new file mode 100644 index 000000000..3eaa6ee2a --- /dev/null +++ b/sunscreen_docs/src/linked/intro/intro.md @@ -0,0 +1,23 @@ +# Introduction + +As we've alluded to, the power of our FHE and ZKP compilers is fully realized when we link them together. + +## Trust and Validation + +*RS: likely need to rework the "trust in trustless settings" piece to better capture the spirit of how fhe+zkp combine. may want to remove the part that alludes to verifiable fhe since we don't support that* + +Our main motivation for linking these concepts together is to allow trust in trustless settings. We've already seen how FHE enables private computation; that is, how one party can encrypt their private data, and how another party can compute on those encrypted inputs and return an encrypted result, with only a public key, and no knowledge of the underlying private data. + +However, without ZKPs, this model of private computation implicitly relies on _trust_ between those two parties. The computing party trusts that the inputs are valid encryptions and that the underlying values are valid for the given computation. The other party has to trust that the computation took place correctly. + +We previously discussed the [inability to perform comparisons](/fhe/fhe_programs/writing_an_fhe_program/limitations.md#comparisons-not-supported) on encrypted data, which are necessary for many input validations, and [how ZKPs could fill this gap](/zkp/compiler/compiler.md#how-do-zkps-and-fhe-fit-together) — in this section, we'll show how to do this in practice. + +## Prerequisites + +The functionality described in this section is gated behind the `linkedproofs` feature flag, so make sure you've enabled it: + +```toml +sunscreen = { version = "*", features = ["linkedproofs"] } +``` + +If you are reading this section, we assume you have also read through the FHE and ZKP sections above. If you have any questions, remember we're available on [Discord](https://discord.com/invite/sunscreen?ref=docs.sunscreen.tech) to chat all things FHE and ZKP! diff --git a/sunscreen_docs/src/linked/linked_programs/limitations.md b/sunscreen_docs/src/linked/linked_programs/limitations.md new file mode 100644 index 000000000..8183672f7 --- /dev/null +++ b/sunscreen_docs/src/linked/linked_programs/limitations.md @@ -0,0 +1,12 @@ +# Limitations + +A ZKP program with linked FHE inputs has the [same +limitations](/zkp/zkp_programs/limitations.md) as unlinked ZKP programs. + +However, keep in mind that when your ZKP program function takes in a linked +input argument, what you can do with that argument is restricted based on its +type. In particular, these types don't directly support +[arithmetic](/zkp/zkp_programs/types.md#native-field-elements) nor +[constraints](/zkp/zkp_programs/constraints.md#constraints) like native field +elements do. To perform these operations, you must first convert the linked +inputs into field elements, as we saw repeatedly in the last section. diff --git a/sunscreen_docs/src/linked/linked_programs/linked_programs.md b/sunscreen_docs/src/linked/linked_programs/linked_programs.md new file mode 100644 index 000000000..24224cc16 --- /dev/null +++ b/sunscreen_docs/src/linked/linked_programs/linked_programs.md @@ -0,0 +1,3 @@ +# What's in a Linked ZKP program? + +This section describes the anatomy of an FHE-linked ZKP program, what you can and can't do, and the different data types that support linking. diff --git a/sunscreen_docs/src/linked/linked_programs/types.md b/sunscreen_docs/src/linked/linked_programs/types.md new file mode 100644 index 000000000..9c7abe572 --- /dev/null +++ b/sunscreen_docs/src/linked/linked_programs/types.md @@ -0,0 +1,102 @@ +# Types + +With the `linkedproofs` feature enabled, the [original ZKP +types](/zkp/zkp_programs/types.md) are extended to include types that mirror +their FHE counterparts. + +## How to link? + +Before we enumerate those types, there are a few traits and attributes to be aware of that determine the ability to link an FHE ciphertext to a ZKP program. + +### Linking + +The `LinkWithZkp` trait is implemented for FHE types (like [Signed][signed], [Unsigned][unsigned], etc.) that supporting linking the private encrypted values as inputs to a ZKP program. This trait uniquely defines the ZKP counterpart type; for example, the following impl + +```rust,ignore +impl LinkWithZkp for sunscreen::types::bfv::Signed { + type ZkpType = sunscreen::types::zkp::BfvSigned; +} +``` + +indicates that when you link a `Signed` value, the corresponding type expected in your ZKP program signature is `BfvSigned` (otherwise you will see a type error). + +### Decoding + +Recall that plaintext values in FHE are actually +[polynomials](/fhe/intro/why.md), and to be cryptographically secure, when you +link an FHE type to a ZKP program, it is actually this plaintext polynomial that is +inserted into the ZKP circuit. + +Of course, if you are linking this value to your ZKP program, you _probably_ don't care about the actual encoding (i.e., the plaintext polynomial coefficients), but rather the _underlying value_ being encoded, whether that's a signed, unsigned, or rational value. This is where the `AsFieldElement` trait comes in: given a `linked_input`, the trait method `linked_input.into_field_elem()` decodes the input into a native field element. + +### Specifying linked inputs + +Linked inputs are handled specially with the `#[linked]` argument attribute. +Such arguments are also inherently private, but they must be specified with the +linked attribute rather than the `#[private]` attribute. In fact, we enforce +that you can use the argument types below _if and only if_ they are adorned with +the `#[linked]` attribute; doing otherwise will result in a compile-time error. + +Lastly, note that `#[linked]` arguments must be specified _before_ all other +argument types (private, public, and constant). + +## Signed + +The counterpart of the FHE [Signed][signed] type is +`BfvSigned`: + +```rust +# use sunscreen::{ +# types::zkp::{AsFieldElement, BfvSigned, ConstrainEq, Field, FieldSpec}, zkp_program +# }; +# use std::ops::Neg; +# +#[zkp_program] +fn is_negation(#[linked] a: BfvSigned, b: Field) { + a.into_field_elem().constrain_eq(b.neg()); +} +``` + +## Unsigned + +The counterpart of the FHE [Unsigned64][unsigned] and [Unsigned128][unsigned] types are +`BfvUnsigned64` and `BfvUnsigned128` respectively:[^1] + +```rust +# use sunscreen::{ +# types::zkp::{AsFieldElement, BfvUnsigned64, BfvUnsigned128, ConstrainCmp, Field, FieldSpec}, zkp_program, zkp_var +# }; +# +#[zkp_program] +fn exceeds(#[linked] a: BfvUnsigned64, #[linked] b: BfvUnsigned128) { + a.into_field_elem().constrain_le_bounded(zkp_var!(u64::MAX), 64); + b.into_field_elem().constrain_le_bounded(zkp_var!(u128::MAX), 128); +} +``` +## Rational + +The counterpart of the FHE [Rational][rational] type is `BfvRational`. This one +is a bit different from the others because the rational type actually encodes +two signed integers, a numerator and a denominator. Consequently, the +`into_field_elem` actually returns two field elements for this type: + +```rust +# use sunscreen::{ +# types::zkp::{AsFieldElement, BfvRational, ConstrainCmp, Field, FieldSpec}, zkp_program +# }; +# +#[zkp_program] +fn compare_rational(#[linked] x: BfvRational, #[linked] y: BfvRational) { + let (x_num, x_den) = x.into_field_elem(); + let (y_num, y_den) = y.into_field_elem(); + let x = x_num * y_den; + let y = y_num * x_den; + x.constrain_le_bounded(y, 128); +} +``` + +[^1]: Note the absence of ZKP types corresponding to larger unsigned integers like `Unsigned256`. This is because a native field element can only be so large, and while the field modulus will vary depending on the proof system, the current default bulletproofs backend has a field modulus around \\( 2^{252} \\) thus won't fit all 256-bit integers. + +[signed]: /fhe/fhe_programs/types/signed.md +[unsigned]: /fhe/fhe_programs/types/unsigned.md +[rational]: /fhe/fhe_programs/types/rational.md diff --git a/sunscreen_docs/src/linked/runtime/prove.md b/sunscreen_docs/src/linked/runtime/prove.md new file mode 100644 index 000000000..77bb510c4 --- /dev/null +++ b/sunscreen_docs/src/linked/runtime/prove.md @@ -0,0 +1,165 @@ +# Proving + +Now that we know how to construct an `FheZkpRuntime`, we can use it to instantiate a `LinkedProofBuilder`: + +```rust +{{#rustdoc_include ../basic_prog.rs:none}} +# fn main() -> Result<(), Error> { +let app = Compiler::new() + .fhe_program(increase_by_factor) + .zkp_backend::() + .zkp_program(is_greater_than_one) + .compile()?; + +let runtime = FheZkpRuntime::new(app.params(), &BulletproofsBackend::new())?; +let (public_key, private_key) = runtime.generate_keys()?; +let mut builder = runtime.linkedproof_builder(); +# Ok(()) +# } +``` + +There are a number of methods on the builder that will help you construct the +proof you need for your particular protocol. We'll walk through some of the more +commonly used methods, but they might not "click" until taking a look at their +usage in the [private transaction example](/linked/applications/private_tx.md). + +#### Encrypt + +Use the `encrypt` method if you need to encrypt a value and prove that (1) the +ciphertext is freshly encrypted, (2) the ciphertext is well formed, and (3) you +know the underlying encrypted message. However, do _not_ use this method if you +also need to link the input to a ZKP program. + +```rust +{{#rustdoc_include ../basic_prog.rs:none}} +# fn main() -> Result<(), Error> { +# let app = Compiler::new() +# .fhe_program(increase_by_factor) +# .zkp_backend::() +# .zkp_program(is_greater_than_one) +# .compile()?; +# +# let runtime = FheZkpRuntime::new(app.params(), &BulletproofsBackend::new())?; +# let (public_key, private_key) = runtime.generate_keys()?; +# let mut builder = runtime.linkedproof_builder(); +let ct = builder.encrypt(&Signed::from(1), &public_key)?; +# Ok(()) +# } +``` + +Similarly you can call `encrypt_symmetric` and provide a private key, if you'd +like to use a symmetric encryption. + +```rust +{{#rustdoc_include ../basic_prog.rs:none}} +# fn main() -> Result<(), Error> { +# let app = Compiler::new() +# .fhe_program(increase_by_factor) +# .zkp_backend::() +# .zkp_program(is_greater_than_one) +# .compile()?; +# +# let runtime = FheZkpRuntime::new(app.params(), &BulletproofsBackend::new())?; +# let (public_key, private_key) = runtime.generate_keys()?; +# let mut builder = runtime.linkedproof_builder(); +let ct = builder.encrypt_symmetric(&Signed::from(1), &private_key)?; +# Ok(()) +# } +``` + +#### Encrypt returning link + +The `encrypt_returning_link` method returns back a `LinkedMessage` in addition +to the `Ciphertext` encryption. Use this method if you need to encrypt a value +and prove that it is (1) freshly encrypted, (2) well formed, (3) you know the +underlying encrypted message, and (4) you want to link the message as an input +to a ZKP program. + +```rust,no_run +{{#rustdoc_include ../basic_prog.rs:none}} +# fn main() -> Result<(), Error> { +# let app = Compiler::new() +# .fhe_program(increase_by_factor) +# .zkp_backend::() +# .zkp_program(is_greater_than_one) +# .compile()?; +# +# let runtime = FheZkpRuntime::new(app.params(), &BulletproofsBackend::new())?; +# let (public_key, private_key) = runtime.generate_keys()?; +# let mut builder = runtime.linkedproof_builder(); +let (ct, link) = builder.encrypt_returning_link(&Signed::from(2), &public_key)?; +let proof = builder + .zkp_program(app.get_zkp_program(is_greater_than_one).unwrap())? + .linked_input(link) + .build(); +# Ok(()) +# } +``` + +Again, you can also use `encrypt_symmetric_returning_link` to do the same thing +for a symmetric encryption. + +#### Decrypt returning link + +Use the `decrypt_returning_link` method if you have an existing ciphertext, +perhaps the result of some FHE program computation, and you want to prove that +(1) the ciphertext is well formed, (2) you know the underlying encrypted +message, and (3) you want to link the message as an input to a ZKP program. + +```rust,no_run +{{#rustdoc_include ../basic_prog.rs:none}} +# fn main() -> Result<(), Error> { +# let app = Compiler::new() +# .fhe_program(increase_by_factor) +# .zkp_backend::() +# .zkp_program(is_greater_than_one) +# .compile()?; +# +# let runtime = FheZkpRuntime::new(app.params(), &BulletproofsBackend::new())?; +# let (public_key, private_key) = runtime.generate_keys()?; +# let mut builder = runtime.linkedproof_builder(); +# let existing_ct = runtime.encrypt(Signed::from(1), &public_key)?; +let (pt, link) = builder.decrypt_returning_link::(&existing_ct, &private_key)?; +let proof = builder + .zkp_program(app.get_zkp_program(is_greater_than_one).unwrap())? + .linked_input(link) + .build(); +# Ok(()) +# } +``` + +#### Re-encrypt + +Lastly, you can use `reencrypt` to take an existing `LinkedMessage` and encrypt it +_again_. This might seem strange at first, but you may find cases where this is +useful. For example, if you need to encrypt the same value under multiple public +keys and you want to show that those ciphertexts are in fact (1) well formed, (2) +freshly encrypted, and (3) encrypt the same underlying value, then this method +will come in handy. Conveniently, all encryptions of a single link will still be +_one message_ to link to the ZKP, so you don't have to provide all of them as +separate linked inputs. + +```rust,no_run +{{#rustdoc_include ../basic_prog.rs:none}} +# fn main() -> Result<(), Error> { +# let app = Compiler::new() +# .fhe_program(increase_by_factor) +# .zkp_backend::() +# .zkp_program(is_greater_than_one) +# .compile()?; +# +# let runtime = FheZkpRuntime::new(app.params(), &BulletproofsBackend::new())?; +# let (my_public_key, my_private_key) = runtime.generate_keys()?; +# let (other_public_key, other_private_key) = runtime.generate_keys()?; +# let mut builder: LinkedProofBuilder = todo!(); +let (ct_my_key, link) = builder.encrypt_returning_link(&Signed::from(2), &my_public_key)?; +let ct_other_key = builder.reencrypt(&link, &other_public_key)?; +let proof = builder + .zkp_program(app.get_zkp_program(is_greater_than_one).unwrap())? + .linked_input(link) + .build(); +# Ok(()) +# } +``` + +It bears repeating that this method _purposefully reveals that two ciphertexts encrypt the same value_! So, use this method with care and only when appropriate. There's an example of using this method in the [private transaction example](/linked/applications/private_tx.md). diff --git a/sunscreen_docs/src/linked/runtime/runtime.md b/sunscreen_docs/src/linked/runtime/runtime.md new file mode 100644 index 000000000..d68fb2984 --- /dev/null +++ b/sunscreen_docs/src/linked/runtime/runtime.md @@ -0,0 +1,27 @@ +# Runtime + +To be frank, proving and verifying ZKP programs with linked inputs is much more complicated than [programs without linked inputs](/zkp/runtime/runtime.md). However, we've done our best to offer a high-level API so that most of the complexity is hidden from the user. This API centers around a "builder" of sorts, that allows you to perform encryptions while building up the prover knowledge. We'll walk through how to use it in the next section. + +Before we get started with proving and verifying, we'll need to instantiate a +runtime. As we noted in the section on +[compiling](/linked/compiling/compiling.md), linking FHE inputs to ZKP programs +requires some FHE context. Thus, instead of using a `ZkpRuntime`, we'll use an +`FheZkpRuntime`: + +```rust +{{#rustdoc_include ../basic_prog.rs:none}} +# fn main() -> Result<(), Error> { +let app = Compiler::new() + .fhe_program(increase_by_factor) + .zkp_backend::() + .zkp_program(is_greater_than_one) + .compile()?; + +let runtime = FheZkpRuntime::new(app.params(), &BulletproofsBackend::new())?; +# Ok(()) +# } +``` + +Once you're created a runtime, you can: +* [make a proof](./prove.md) +* [verify a proof](./verify.md) diff --git a/sunscreen_docs/src/linked/runtime/serialization.md b/sunscreen_docs/src/linked/runtime/serialization.md new file mode 100644 index 000000000..4ad6e601d --- /dev/null +++ b/sunscreen_docs/src/linked/runtime/serialization.md @@ -0,0 +1,28 @@ +# Serialization + +Serializing works just like [unlinked proofs](/zkp/runtime/serialization.md), +but for the sake of completeness, below is an example of serialization and +deserialization of a linked proof: + +```rust,no_run +{{#rustdoc_include ../basic_prog.rs:none}} +# fn main() -> Result<(), Box> { +# let app = Compiler::new() +# .fhe_program(increase_by_factor) +# .zkp_backend::() +# .zkp_program(is_greater_than_one) +# .compile()?; +# +# let runtime = FheZkpRuntime::new(app.params(), &BulletproofsBackend::new())?; +# let (public_key, private_key) = runtime.generate_keys()?; +# let mut builder = runtime.linkedproof_builder(); +let (ct, link) = builder.encrypt_returning_link(&Signed::from(2), &public_key)?; +let proof = builder + .zkp_program(app.get_zkp_program(is_greater_than_one).unwrap())? + .linked_input(link) + .build()?; +let serialized_proof = bincode::serialize(&proof)?; +let deserialized_proof: LinkedProof = bincode::deserialize(&serialized_proof)?; +# Ok(()) +# } +``` diff --git a/sunscreen_docs/src/linked/runtime/verify.md b/sunscreen_docs/src/linked/runtime/verify.md new file mode 100644 index 000000000..1f3b457c6 --- /dev/null +++ b/sunscreen_docs/src/linked/runtime/verify.md @@ -0,0 +1,37 @@ +# Verifying + +Verifying a linked proof looks just like proving one. You'll call the same +methods on the `LinkedProofVerificationBuilder` that you did on the +`LinkedProofBuilder`, in the same order, but instead of supplying the private +values, you'll supply the public ones. Then you'll specify the proof, ZKP +program, and any public or constant inputs, as we did for the [unlinked ZKP +programs](/zkp/runtime/verify.md). + +```rust,no_run +{{#rustdoc_include ../basic_prog.rs:none}} +# fn main() -> Result<(), Error> { +let app = Compiler::new() + .fhe_program(increase_by_factor) + .zkp_backend::() + .zkp_program(is_greater_than_one) + .compile()?; +let runtime = FheZkpRuntime::new(app.params(), &BulletproofsBackend::new())?; +let (public_key, private_key) = runtime.generate_keys()?; + +let mut proof_builder = runtime.linkedproof_builder(); + +let (ct, link) = proof_builder.encrypt_returning_link(&Signed::from(2), &public_key)?; +let proof = proof_builder + .zkp_program(app.get_zkp_program(is_greater_than_one).unwrap())? + .linked_input(link) + .build()?; + +let mut verify_builder = runtime.linkedproof_verification_builder(); +verify_builder.encrypt_returning_link::(&ct, &public_key)?; +verify_builder + .proof(proof) + .zkp_program(app.get_zkp_program(is_greater_than_one).unwrap())? + .verify()?; +# Ok(()) +# } +``` diff --git a/sunscreen_docs/src/zkp/compiler/compiler.md b/sunscreen_docs/src/zkp/compiler/compiler.md index 427ffa077..99b407452 100644 --- a/sunscreen_docs/src/zkp/compiler/compiler.md +++ b/sunscreen_docs/src/zkp/compiler/compiler.md @@ -6,9 +6,7 @@ One aspect of this was deciding whether we should develop an entirely new langua In ZKP land, the engineer needs to translate their higher level program into a format that ZKPs can understand (arithmetic circuits/constraint systems). This process is called arithmetization and there are a few different ways to specify constraints. Our compiler currently uses R1CS (Rank 1 Constraint System) and helps automate this process for you. -We currently support [Bulletproofs](https://github.com/zkcrypto/bulletproofs) as the proof backend though we will add support for other proof backends in the future. Bulletproofs allow you to prove general relations (using arithmetic circuits) and does not require a trusted setup. The main reason for targetting Bulletproofs was speed to launch (as Bulletproofs will most easily link with FHE even though it's by no means the most efficient proof system). - -**WARNING: Our FHE and ZKP compilers are not _yet_ linked together!** Specifically, our current API does not allow you to prove facts about FHE ciphertexts. +We currently support [Bulletproofs](https://github.com/zkcrypto/bulletproofs) as the proof backend though we will add support for other proof backends in the future. Bulletproofs allow you to prove general relations (using arithmetic circuits) and does not require a trusted setup. The main reason for targeting Bulletproofs was speed to launch (as Bulletproofs will most easily link with FHE even though it's by no means the most efficient proof system). ## What features does our compiler offer? This list isn't comprehensive (and may not mean much to you unless you've worked with ZKPs previously). These are just the main features we'd like to call attention to: @@ -22,10 +20,10 @@ This list isn't comprehensive (and may not mean much to you unless you've worked ## How do ZKPs and FHE fit together? -ZKPs are useful (and often necessary) when building general purpose FHE-enabled -applications in a trustless environment. By trustless, we mean an environment in which we either (1) can't fully trust the user encrypting their data or (2) can't fully trust the party responsible for performing the computation on the encrypted data. +ZKPs are useful (and often necessary) when building general purpose FHE-enabled applications in a trustless environment. By trustless, we mean an environment in which we either (1) can't fully trust the user encrypting their data or (2) can't fully trust the party responsible for performing the computation on the encrypted data. What are some examples of the former situation? Let's suppose you've used our [FHE compiler](https://github.com/Sunscreen-tech/Sunscreen) to implement private transactions. If a user wants to withdraw some encrypted amount `enc(amt)` from their encrypted balance `enc(bal)`, how can the implementation enforce that `amt <= bal` while allowing `amt` and `bal` to stay private? Enter ZKPs! Additionally, validating that the user-provided ciphertext is "well-formed" (and not just some random garbage) might be important. In this scenario, the user can send a ZKP along with their ciphertexts, proving the ciphertexts' validity without revealing the underlying values. -With regards to the latter situation, let's take a step back and think about how things work in web2. Maybe you've used AWS to run a benchmark or you've experimented with ChatGPT. How do we know that Amazon has actually run the computation you asked them to? How do we know that OpenAI has actually used their most advanced learning model? The answer is we don't. We're *trusting* these organizations to have done what we asked them to based on their reputation. If we ask AWS to run an FHE computation for us, we likely trust that they've run it correctly. However, in web3, random parties may be tasked with running FHE computations. If we're assuming a large number of parties are all running the same FHE computation, it may be sufficient to assume there is an honest majority of them (no ZKP required). On the other hand, if a single party is taked with running an FHE computation (say a rollup provider), that party will need to *prove* that they've run the computation correctly. +With regards to the latter situation, let's take a step back and think about how things work in web2. Maybe you've used AWS to run a benchmark or you've experimented with ChatGPT. How do we know that Amazon has actually run the computation you asked them to? How do we know that OpenAI has actually used their most advanced learning model? The answer is we don't. We're *trusting* these organizations to have done what we asked them to based on their reputation. If we ask AWS to run an FHE computation for us, we likely trust that they've run it correctly. However, in web3, random parties may be tasked with running FHE computations. If we're assuming a large number of parties are all running the same FHE computation, it may be sufficient to assume there is an honest majority of them (no ZKP required). On the other hand, if a single party is tasked with running an FHE computation (say a rollup provider), that party will need to *prove* that they've run the computation correctly. +For more examples of using FHE and ZKPs together, take a look at our [private transactions example](/linked/examples/private_tx.md). diff --git a/sunscreen_docs/src/zkp/faq/faq.md b/sunscreen_docs/src/zkp/faq/faq.md index 863b60b2a..47bf672bc 100644 --- a/sunscreen_docs/src/zkp/faq/faq.md +++ b/sunscreen_docs/src/zkp/faq/faq.md @@ -3,12 +3,10 @@ ### Why did you create your own ZKP compiler? We created our own ZKP compiler mainly to ensure compatibility with our FHE compiler—existing ZKP compilers were not designed with FHE's needs in mind. -### How will this fit in with Sunscreen's FHE compiler? -Our ZKP compiler is currently offered as a standalone product. - -In the future, Sunscreen's FHE compiler and ZKP compiler will be linked together so that you can prove statements about FHE-encrypted inputs! This is especially important in trustless settings like web3. You will still be able to use either of these offerings independently if desired. - -As part of linking together our FHE and ZKP compiler, we're working on an implementation of a proof system that allows us to (somewhat efficiently) show that FHE ciphertexts are well-formed. This proof system is called [Short Discrete Log Proofs for FHE and Ring-LWE Ciphertexts](https://eprint.iacr.org/2019/057) (SDLP). Once we have linked our FHE compiler with SDLP and SDLP with our ZKP compiler, you'll be able to use our FHE and ZKP compilers together. +### How does this fit in with Sunscreen's FHE compiler? +While our ZKP compiler can be used as a standalone product, it is uniquely +useful when used in [conjunction with our FHE compiler](/linked/intro/intro.md) to prove statements about +FHE-encrypted inputs! ### Why Bulletproofs as the proof backend? Aren't there more performant proof systems? As mentioned earlier, our ZKP compiler was designed with the end goal of it being used in conjunction with our FHE compiler. diff --git a/sunscreen_docs/src/zkp/zkp_programs/attributes.md b/sunscreen_docs/src/zkp/zkp_programs/attributes.md index 8bb803af6..0aae1939e 100644 --- a/sunscreen_docs/src/zkp/zkp_programs/attributes.md +++ b/sunscreen_docs/src/zkp/zkp_programs/attributes.md @@ -43,3 +43,9 @@ threshold for issuing transactions. We do not discuss these in the main docs; please see the [advanced section](../advanced/constant_inputs.md) if you're interested in working with constant arguments. + +## Linked + +Lastly, there is also a `#[linked]` attribute available when the `linkedproofs` +feature is enabled. This attribute is used when linking together our FHE and ZKP +compilers; see the [linked section](/linked/intro/intro.md) for more details.