Skip to content

Commit

Permalink
blockchain + publisher runthrough on zkhack montreal presentation (#3)
Browse files Browse the repository at this point in the history
* initial, still gotta do one final runthrough for cleanup

* small edits

* done with edits

* added final slides and formatting
  • Loading branch information
sashaaldrick authored Aug 9, 2024
1 parent 84611e1 commit 518aefe
Show file tree
Hide file tree
Showing 6 changed files with 335 additions and 0 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added content/risc-zero/zk-hack-montreal/img/rzup.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
335 changes: 335 additions & 0 deletions content/risc-zero/zk-hack-montreal/slides.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,341 @@ Notes:
---

<img rounded style="width: 30%;" src="./img/hardhat.png" />

Notes:

So you’ve heard about why you might want to use ZK and therefore, why you should use RISC Zero’s zkVM. Nuke’s done an excellent job there.
For my part of the presentation, let’s all put our blockchain developer hat on. For the longest time, this hat meant using Hardhat.

---

<img rounded style="width: 60%;" src="./img/foundry_meme.png" />

Notes:

The developer framework that's all the rage these days, and one that we are very fond of at RISC Zero is Foundry. Unfortunately, doesn't fit into my blockchain developer hat metaphor as we've switched to only metal metaphors becase of Rust.

---

<img rounded style="width: 60%;" src="./img/foundry_banner.png" />

Notes:

And its because of Rust, that Foundry integrates very well into the RISC Zero zkVM stack. We love it so much that we want to make developer's life easier and so we've created the Risc Zero Foundry template.

---

<img rounded style="width: 75%;" src="./img/foundry_template.png" />

Notes:

You simply clone this repo and run a few commands to get started. Let's walkthrough that quickly and then we can get to the meat of understanding what's going on.

---

```bash
git clone https://github.com/risc0/risc0-foundry-template.git
```

---

```bash
curl -L https://risczero.com/install | bash

rzup

cargo risczero —version
```

Notes:

To install Rust or Foundry, you use the really handy `rustup` or `foundryup`.

Well, now just like rustup and foundryup, you can type: curl -L https://risczero.com/install | bash followed by rzup. You can run cargo risczero —version to make sure everything installed correctly.

---

<img rounded style="width: 60%;" src="./img/rzup.png" />

Notes:

Now that we have the RISC Zero toolchain installed, let's get into how this foundry template example works.
Let’s jump straight to contracts/EvenNumber.sol and straight to the function that requires a proof to be verified.

---

```solidity [1,3|2,4]
function set(uint256 x, bytes calldata seal) public {
bytes memory journal = abi.encode(x);
verifier.verify(seal, imageId, sha256(journal));
number = x;
}
```

Notes:

Let's run through this function and its arguments.

Interestingly, we don't know what the verification is here, especially with a strange function name called 'set' and no comments (though I removed those for the presentation). Why is that? Well, we've offloaded computation here from the EVM to Risc Zero's zkVM.

Let's have a look at a function that does the exact same thing directly in Solidity.

---

```solidity
function set(uint256 x) public {
require(x % 2 == 0, "Not an even number");
number = x;
}
```

Notes:

What are we doing here? This function is way easier to understand as its all done directly in Solidity.

All we're doing here is checking if an input number is even, and if so, update the current variable number to that new proven even number.

So what was all the journal and seal about? Let's go back to it

---

```solidity
function set(uint256 x, bytes calldata seal) public {
bytes memory journal = abi.encode(x);
verifier.verify(seal, imageId, sha256(journal));
number = x;
}
```

Notes:

At first glance, we look like we’ve actually complicated things, after all the function with the require statement doesn’t require strange arguments like a `seal`, or to create a `journal`. Sounds like we are working in a medieval library.

Thankfully, we live in the 21st century post the discovery of zero knowledge cryptography, so just like our medieval ancestors lamented about their lack of ability to take compute offchain, we can lament that we have silly variable names like `journal` and `seal`. I know which choice I would take any day.

Back to the matter at hand, these two functions carry out the same computation (checking a number is even) but that computation is not carried out in the same place, or on the same ‘virtual machine’. One is the EVM, and the other is RISC Zero’s zkVM. We can see that the function that utilises the zkVM for checking a number is even, requires an extra input argument called the `seal`.

---

# Seal

- The seal is a zk-STARK or zk-SNARK.
- It cryptographically attests to the correct execution of the `guest program`.
- The `guest program` is checking the parity of `x` --> proof.

Notes:

The `seal` is either a STARK or a SNARK generated by the prover (a party offchain, we’ll delve into the specifics of Bonsai as a coprocessor later). The `seal` cryptographically attests to correct execution of the `guest program` as well as the outputs of that guest program. The `guest program` is a Rust program which takes an input number, `x` and checks if `x` is divisible by 2, if so the computation executes successfully and a proof is generated.

So we have the `seal`, in this case as we’re dealing with an onchain environment, it’s a SNARK. SNARKs are smaller proofs compared to STARKs, making them more gas-efficient for onchain verification.

---

# Journal

- Contains the public outputs of the computation

```solidity [2|1-5]
function set(uint256 x, bytes calldata seal) public {
bytes memory journal = abi.encode(x);
verifier.verify(seal, imageId, sha256(journal));
number = x;
}
```

Notes:

The journal contains the public outputs of the computation. We’ll see later on that we used Solidity’s ABI encoding when ‘committing` x to the journal. This is done to make decoding information easier on the Solidity side of things once we’re in the app contract as we are here.

We are taking a number x in the input of the solidity function, we’d like to make sure that this number x is the one that was checked to be even in the guest program. For this reason, we actually reconstruct the journal onchain here, and pass that through to the verify function. If the journal does not match the proof, verify will fail. So that’s a handy way of making sure that everything is going smoothly.

A quick note, this reconstruction of the journal is not always feasible. This example is straightforward, and handles only one number variable. Most real world applications, including those that you’ll build yourself, will have a higher degree of complexity. In those cases, recreating the journal onchain might seem counterintuitive in a world where we are trying to save gas. In most cases, passing the journal through as an argument and decoding that onchain to have some sanity checks would be the better way of doing it. This will become a lot clearer later, when Nuke comes back on to walk you through the guest program specifically.

---

# Verification

```solidity [3|1-5]
function set(uint256 x, bytes calldata seal) public {
bytes memory journal = abi.encode(x);
verifier.verify(seal, imageId, sha256(journal));
number = x;
}
```

Notes:

Verification is handled by RISC Zero’s verification contract, which you can find deployed across many different chains. In our application contract, the verification contract address is instantiated at deploy time as a constructor argument. The verification contract is actually a proxy contract, and so you can be sure if any new features are added to the verification contract, this address will stay valid in your application.

verify takes the seal or the proof, the imageId and a hash of the journal. The proof is verified and the imageId and journal variables here attest that the correct ELF binary was run in the zkVM with the corresponding identifier imageId, and the correct outputs were calculated within the zkVM via the journal. Note that if anything is wrong, the verify function will revert and the error will be bubbled up through require statements, which you can see with the likes of Tenderly simulations when debugging on testnets before deploying to production on mainnet.

Going back to the function as a whole, and you can now see that, given the guest program does indeed check if a number is even, that the two functions that were shown previously are in fact identical in their conclusion: only update the state of number if it’s even.

---

# Why?

- Doesn't this seem a little overkill?

Notes:

This may all seem overkill for checking if a number is even, and you can be forgiven for thinking that, but actually if you benchmark testing 1 number, 10 numbers, 100 numbers and so on, I’d be interested to see hands up for how many numbers it takes before it becomes pretty much unfeasible to do this simple computation (albeit repeatedly) onchain.

Thankfully, we don’t have to guess and I wrote a simple contract that modified what we saw here today. Hopefully, this gets across why (and how badly) ZK is needed for scaling compute onchain.

---

# Gas Benchmarks

- [PASS] testGas1Number() (gas: 71015)
- [PASS] testGas10Numbers() (gas: 259748) --> $10
- [PASS] testGas1000Numbers() (gas: 23083559) --> $900
- [PASS] testGas10000Numbers() (gas: 231464264)

Notes:

In this example, we are checking an array of numbers onchain, and saving them to a results array if they’re even. So to check 10 numbers, it costs 260k gas here.

On L1, at an ETH price of $2500$, with a gas price of around 15 gwei, 260k gas costs around $10. Each number is costing you one dollar. Checking 1000 numbers at 23M gas, is probably impossible unless you’re some sort of whale with your own large amount of validators to help inclusion, but thats just under 900 dollars.

Think to your personal laptop from 10 years ago, that thing could do this calculation is probably nanoseconds. Food for thought.

---

# App

<img rounded style="width: 60%;" src="./img/risc0-ethereum-bonsai.png" />

Notes:

Back to zkVM reality.

We’ve walked through the `Ethereum` side of this image, and to some extent, you can understand what our proving API, `Bonsai` handles from the explanation and the code we’ve walked through already.

The middle part of the diagram, which is labelled `app`, is a crucial part of any application utilising RISC Zero’s zkVM. In the foundry template, you can find its source code in `apps/src/bin` under `publisher.rs`.

As the name suggests, the main _end_ purpose of this code is to _publish_ a proof to your application contract where it’s needed for verification for some state update, i.e. `EvenNumber.sol`'s `set` function. In practice, this means sending a transaction onchain with the required arguments. But in fact, _publishing_ is just one part of the publisher, and the diagram also shows that it handles the request for a proof from Bonsai first and acts as middleware to receive that proof and package it up nicely to send to your app contract.

Let’s walk through the main aspects of the `publisher` app in the Foundry Template.

---

# App CLI

```bash
cargo run --bin publisher -- \
--chain-id=11155111 \
--rpc-url=https://eth-sepolia.g.alchemy.com/v2/${ALCHEMY_API_KEY:?} \
--contract=${EVEN_NUMBER_ADDRESS:?} \
--input=12345678
```

Notes:

We have 4 arguments, `chainId`, `rpc-url`, `contract` address and `input`. These arguments are all related to your application contract. This will tell the publisher app what chain you are using, what RPC url to use to talk to that chain, the contract address of your application and the input argument i.e. prove the parity of this input number.

---

# Proving Options

```rust
let receipt = default_prover()
.prove_with_ctx(
env,
&VerifierContext::default(),
IS_EVEN_ELF,
&ProverOpts::groth16(),
)?
.receipt;
```

Notes:

That’s fine, interfacing with the chain in ethers.rs or now alloy, also relatively straightforward. So all we have left is interfacing with Bonsai, and RISC Zero makes that directly available from the `default_prover()` object.

The docstrings are very helpful here and the RISC Zero team do a great job on that, if you hover over default_prover(), you get a full explanation (next slide).

---

# Proving Options 🤌

- `BonsaiProver` if the `BONSAI_API_URL` and `BONSAI_API_KEY` environment variables are set unless `RISC0_DEV_MODE` is enabled.

- `LocalProver` if the `prove` feature flag is enabled.

Notes:

If environment variables BONSAI_API_URL and BONSAI_API_KEY are set, Bonsai will be used automagically for proving, directly from the proving method.

---

# Get the seal/journal from the receipt

```rust
// Encode the seal with the selector.
let seal = groth16::encode(receipt.inner.groth16()?.seal.clone())?;

// Extract the journal from the receipt.
let journal = receipt.journal.bytes.clone();
```

Notes:

Back to publisher.rs, and our trusty seal and journal which we can extract from the receipt that proving on bonsai returns.

---

# Decoding the journal

```rust
let x = U256::abi_decode(&journal, true).context("decoding journal data")?;
```

Notes:

Upon receiving the proof, the app decodes the journal to extract the verified number. This ensures that the number being submitted to the blockchain matches the number that was verified off-chain.

---

# Constructing the calldata

```rust
let calldata = IEvenNumber::IEvenNumberCalls::set(IEvenNumber::setCall {
x,
seal: seal.into(),
})
.abi_encode();
```

Notes:

## Using the IEvenNumber interface, the application ABI-encodes the function call for the 'set' function of the EvenNumber contract. This call includes the verified number, and the seal (proof).

# Summary

- We've used RISC Zero's zkVM for an onchain app.
- We've carried out computation offchain and seen it saves _a lot_ of gas.
- Gas is expensive.

Notes:

To sum up, we’ve used RISC Zero’s zkVM for an onchain app, specifically to verify computation offchain and save a lot of gas. We installed RISC Zero’s toolchain using rzup, installed Foundry Template, we’ve seen that gas is really expensive and we’ve walked through the Solidity side of things with EvenNumber.sol representing the onchain part of where you want to save gas and how to verify proofs generated by the zkVM onchain.

---

# Publisher - super important ‼️

<img rounded style="width: 60%;" src="./img/risc0-ethereum-bonsai.png" />

Notes:

Finally, we’ve seen the importance of the publisher as the main orchestrator requesting the proof from Bonsai, interacting with the application contract (a lot of the relevant parameters are specified via input arguments to the publisher CLI), and actually being the ‘backend’ of offloading the computation from the EVM over to the zkVM.

Now I’ll hand back over to Nuke to discuss the specifics of the zkVM in more detail. Thanks.

---

# ✨ Inspiration

> #### ⚠️ &nbsp; Do **not** copy 🍝 &nbsp; ⚠️
Expand Down

0 comments on commit 518aefe

Please sign in to comment.