Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions browser-verify/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "browser-verify"
version = "0.1.0"
edition = "2024"

[dependencies]
borsh = { version = "1.5.7", default-features = false }
guest = { path = "guest" }
risc0-zkvm = { version = "3.0.3", features = ["prove"] }
serde = "1.0"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

[workspace]
resolver = "2"
members = ["guest", "verifier"]

[features]
cuda = ["risc0-zkvm/cuda"]
103 changes: 103 additions & 0 deletions browser-verify/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# R0VM Browser Verifier

This repository contains a full-stack example for an in-browser ZK proof verifier.

The stack:
- [R0VM](https://dev.risczero.com/api/zkvm/quickstart) for ZK proving.
- [Next.JS](https://nextjs.org/) for the browser app.
- [wasm-pack](https://github.com/rustwasm/wasm-pack) for building the [verifier library](./rust/verifier/src/lib.rs) to [browser compatible JS](./web/public/wasm/pkg/package.json).

## Video Walkthrough

[![Watch the video walkthrough](/web/public/thumbnail.png)](https://www.youtube.com/watch?v=aTCPCf8ff-c)

## Project Structure

The project is organized like below:

```
browser_verifier/
├── guest/ # R0VM guest code that gets executed and proven
├── host/ # R0VM host code for generating proofs
├── verifier/ # WebAssembly-compatible proof verification library
└── web/ # Next.js web application for in-browser proof verification
```

### Running the Web App

```bash
cd web
npm run dev
```

Open [http://localhost:3000](http://localhost:3000) to view the application.

The `web/` directory contains a Next.js application that demonstrates in-browser proof verification:

- Loads the WebAssembly verifier module from [pkg](./web/public/wasm/pkg/) (see [Building the Verifier](#building-the-verifier)).
- Fetches the proof and image ID from [binary files](./web/public/proof_data/).
- Provides UI for proof verification.
- Displays verification result and time it took.
- Displays a time comparison between direct calculation and proof verification.

The web interface allows users to:
1. Calculate the 1000000th Fibonacci number directly in the browser.
2. Verify a pre-generated proof of the calculation in a fraction of the time.
3. Compare the performance difference between direct calculation and proof verification.

## Rust Components

### guest/

The guest code contains the program that runs inside the R0VM (RISC Zero Virtual Machine). This is the computation that gets proven. In this project, it calculates the 1000000th Fibonacci number and commits the length of that number to the journal.

### host/

The host code is responsible for:
- Loading and executing the guest program in the R0VM
- Generating proofs of correct execution
- Serializing the proof and the imageID into binary files that can be used by the verifier in the browser.

The host serializes these into 'receipt.bin' and 'image_id.bin' in the web application's public folder.

### Running proving yourself

> 💡 **Tip: If you want to modify the example to run proving yourself**
> This example's host/guest use R0VM v2.0.2; if you want to run proving yourself, before following the instructions below, make sure to switch to the correct branch: `git checkout release-2.0`

To run the host yourself, with proving:

```bash
cargo run --release
```

This will overwrite the proof files in the web app which could cause failure. The proof data files are included directly in the repo, so you shouldn't need to do this unless you're modifying things. The host is set up to save the necessary files straight to the correct `web/public/proof_data` folder. After you run the proving, you can go ahead and run the web app again with `npm run dev` and click 'Verify a Proof'.

### verifier/

The verifier is a Rust library that:
- Takes the proof and image ID as input
- Verifies that the guest program was executed correctly
- Is compiled to WebAssembly for in-browser verification
- Exposes the `verify_proof` function to JavaScript

The [verifier library](src/lib.rs) contains one function: `verify_proof`.

`verify_proof`:
- takes two `u8` arrays as input; `proof_bytes` and `image_id_bytes`.
- using `risc0_zkvm::Receipt`, the [Receipt](https://docs.rs/risc0-zkvm/latest/risc0_zkvm/struct.Receipt.html) struct is built from `proof_bytes`.
- using `risc0_zkvm::sha::Digest`, the [image_id](https://dev.risczero.com/terminology#image-id) digest is built from `image_id_bytes`.
- the receipt is verified using [receipt.verify](https://docs.rs/risc0-zkvm/latest/risc0_zkvm/struct.Receipt.html#method.verify).
- the output is logged to the console using [web_sys](https://rustwasm.github.io/wasm-bindgen/web-sys/using-web-sys.html).

#### Building the Verifier

To compile the verifier library to WebAssembly:

```bash
cd verifier
wasm-pack build --release --target web --out-dir ../web/public/wasm/pkg
```

The output is in the `wasm/pkg/` directory in the public folder for the web app.

10 changes: 10 additions & 0 deletions browser-verify/guest/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "guest"
version = "0.1.0"
edition = "2021"

[build-dependencies]
risc0-build = "3.0.3"

[package.metadata.risc0]
methods = ["fibonacci"]
17 changes: 17 additions & 0 deletions browser-verify/guest/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright 2025 RISC Zero, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

fn main() {
risc0_build::embed_methods();
}
11 changes: 11 additions & 0 deletions browser-verify/guest/fibonacci/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "fibonacci"
version = "0.1.0"
edition = "2021"

[workspace]

[dependencies]
risc0-zkvm = { version = "3.0.3", features = ["std"] }
num-bigint = "0.4"
num-traits = "0.2"
50 changes: 50 additions & 0 deletions browser-verify/guest/fibonacci/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright 2025 RISC Zero, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use num_bigint::BigUint;
use num_traits::{One, Zero};
use risc0_zkvm::guest::env;

/// fast-doubling fib in O(log n) multiplications
fn fib_pair(n: u32) -> (BigUint, BigUint) {
if n == 0 {
(BigUint::zero(), BigUint::one())
} else {
let (a, b) = fib_pair(n >> 1);
// F(2k) = F(k) * (2·F(k+1) – F(k))
let two_b_minus_a = &b * 2u32 - &a;
let c = &a * two_b_minus_a;
// F(2k+1) = F(k)^2 + F(k+1)^2
let d = &a * &a + &b * &b;
if n & 1 == 0 {
(c, d)
} else {
(d.clone(), c + d)
}
}
}

fn main() {
let n: u32 = env::read();

// compute F_n exactly
let (f_n, _) = fib_pair(n);

// decimal-serialize and count digits
let s = f_n.to_str_radix(10);
let digit_count = s.len() as u32;

// only commit the digit count
env::commit(&digit_count);
}
14 changes: 14 additions & 0 deletions browser-verify/guest/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright 2025 RISC Zero, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
include!(concat!(env!("OUT_DIR"), "/methods.rs"));
4 changes: 4 additions & 0 deletions browser-verify/rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[toolchain]
channel = "1.89"
components = ["rustfmt", "rust-src"]
profile = "minimal"
82 changes: 82 additions & 0 deletions browser-verify/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright 2025 RISC Zero, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::{
env,
fs::{File, create_dir_all},
io::Write,
};

use borsh::to_vec;
use guest::FIBONACCI_ELF;
use risc0_zkvm::{ExecutorEnv, ProverOpts, VerifierContext, compute_image_id, default_prover};

fn in_dev_mode() -> bool {
env::var("RISC0_DEV_MODE").is_ok_and(|v| v == "1")
}

fn main() {
// Initialize tracing. In order to view logs, run `RUST_LOG=info cargo run`
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env())
.init();

let input: u32 = 10000;
let env = ExecutorEnv::builder()
.write(&input)
.unwrap()
.build()
.unwrap();

let receipt = default_prover()
.prove_with_ctx(
env,
&VerifierContext::default(),
FIBONACCI_ELF,
&ProverOpts::succinct(),
)
.expect("Proving failed")
.receipt;

let output: u32 = receipt.journal.decode().unwrap();
println!("Fibonacci({}) has {} digits", input, output);

// Skip the file-write section when RISC0_DEV_MODE=1
if in_dev_mode() {
return;
}

// serialize the receipt into bytes
let receipt_bytes = to_vec(&receipt).expect("Serialization failed");

// compute the image ID
let image_id_digest = compute_image_id(FIBONACCI_ELF).expect("Compute Image ID failed");
let image_id_bytes = image_id_digest.as_bytes();

// make sure data dir exists
create_dir_all("web/public/proof_data/").expect("Couldn't create data dir in web app");

// save imageID bytes + receipt bytes to file
let mut receipt_file =
File::create("web/public/proof_data/receipt.bin").expect("Couldn't create receipt.bin");
receipt_file
.write_all(&receipt_bytes)
.expect("Couldn't write receipt data to receipt.bin");

let mut image_id_file =
File::create("web/public/proof_data/image_id.bin").expect("Couldn't create image_id.bin");
image_id_file
.write_all(image_id_bytes)
.expect("Couldn't write image ID to image_id.bin")
}
1 change: 1 addition & 0 deletions browser-verify/verifier/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/target
13 changes: 13 additions & 0 deletions browser-verify/verifier/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "verifier"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
borsh = { version = "1.5.7", default-features = false }
risc0-zkvm = { version = "3.0.3", default-features = false }
wasm-bindgen = "0.2.100"
web-sys = { version = "0.3", features = ["console"] }
45 changes: 45 additions & 0 deletions browser-verify/verifier/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright 2025 RISC Zero, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use borsh::BorshDeserialize;
use risc0_zkvm::{sha::Digest, Receipt};
use wasm_bindgen::prelude::*;
use web_sys::console;

#[wasm_bindgen]
pub fn verify_proof(proof_bytes: &[u8], image_id_bytes: &[u8]) -> Result<u32, JsValue> {
// deseralize proof into Receipt
let receipt = Receipt::try_from_slice(proof_bytes)
.map_err(|e| JsValue::from(format!("Bad receipt: {e}")))?;

// decode and log journal value
let value: u32 = receipt
.journal
.decode()
.map_err(|e| JsValue::from(format!("Journal decode: {e}")))?;
console::log_1(&format!("Decoded journal value = {value}").into());

// deseralize and log image ID
let image_id = Digest::try_from_slice(image_id_bytes)
.map_err(|_| JsValue::from("ImageID not 32 bytes"))?;
console::log_1(&format!("Image ID: 0x{image_id}").into());

// verify proof
receipt
.verify(image_id)
.map_err(|e| JsValue::from(format!("Verify failed: {e}")))?;

console::log_1(&"Proof verified successfully".into());
Ok(value)
}
Loading