-
Notifications
You must be signed in to change notification settings - Fork 31
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
WEB3-79: Refactor of Governance Example for RISC Zero zkVM 1.0 (#197)
This is a refactor of the original governance example located in release-0.20 [here](https://github.com/risc0/risc0/tree/release-0.20/bonsai/examples/governance). This has been rewritten to match the foundry template layout, amongst many other features. At a high level, this PR: - upgrades governance example guest to zkVM 1.0 - deprecates Relay workflow and contracts - updates publisher to handle deprecated relay workflows - simplifies test logic for readability - creates gas benchmarks from these tests For a high level walkthrough, please see [README.md](https://github.com/risc0/risc0-ethereum/blob/a5ccf7872c3d8dfe86d42816b2388da4e27af4c3/examples/governance/README.md), and to run tests locally with benchmarking, see [instructions.md](https://github.com/risc0/risc0-ethereum/blob/a5ccf7872c3d8dfe86d42816b2388da4e27af4c3/examples/governance/instructions.md). --------- Co-authored-by: Angelo Capossele <[email protected]> Co-authored-by: Victor Graf <[email protected]>
- Loading branch information
1 parent
8498f7a
commit 1bb1baf
Showing
32 changed files
with
2,494 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
# autogenerated test data | ||
contracts/test/benchmarks/gas_data.csv | ||
|
||
# Compiler files | ||
cache/ | ||
out/ | ||
|
||
# Ignores development broadcast logs | ||
!/broadcast | ||
/broadcast/*/31337/ | ||
/broadcast/*/11155111/ | ||
/broadcast/**/dry-run/ | ||
|
||
# Ignores anvil logs | ||
anvil_logs.txt | ||
|
||
# Autogenerated contracts | ||
contracts/src/ImageID.sol | ||
contracts/src/Elf.sol | ||
|
||
# Cargo | ||
target/ | ||
|
||
# Misc | ||
.DS_Store | ||
.idea |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
[workspace] | ||
resolver = "2" | ||
members = ["apps", "methods"] | ||
exclude = ["lib"] | ||
|
||
[workspace.package] | ||
version = "0.1.0" | ||
edition = "2021" | ||
|
||
[workspace.dependencies] | ||
# Intra-workspace dependencies | ||
risc0-build-ethereum = { path = "../../build" } | ||
risc0-ethereum-contracts = { path = "../../contracts" } | ||
|
||
# risc0 monorepo dependencies. | ||
risc0-build = { git = "https://github.com/risc0/risc0", branch = "main", features = ["docker"] } | ||
risc0-zkvm = { git = "https://github.com/risc0/risc0", branch = "main", default-features = false } | ||
risc0-zkp = { git = "https://github.com/risc0/risc0", branch = "main", default-features = false } | ||
|
||
alloy-primitives = { version = "0.7.7", default-features = false, features = ["rlp", "serde", "std"] } | ||
alloy-sol-types = { version = "0.7.7" } | ||
anyhow = { version = "1.0.75" } | ||
bincode = { version = "1.3" } | ||
bytemuck = { version = "1.14" } | ||
hex = { version = "0.4" } | ||
log = { version = "0.4" } | ||
governance-methods = { path = "./methods" } | ||
serde = { version = "1.0", features = ["derive", "std"] } | ||
tracing-subscriber = { version = "0.3", features = ["env-filter"] } | ||
tokio = { version = "1.39", features = ["full"] } | ||
url = { version = "2.5" } | ||
|
||
[profile.release] | ||
debug = 1 | ||
lto = true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
# RISC Zero Governor | ||
|
||
To run this example on your machine, follow these [instructions]. | ||
|
||
## Abstract | ||
|
||
This example contains a modified version of OpenZeppelin's [Governor] example, which is the standard for DAO Governance. The modifications have one *end goal*: to **save gas** by taking gas intensive computations *offchain*, using RISC Zero's [zkVM], while maintaining the same trust assumptions. | ||
|
||
## How can we take computation offchain and still trust it? | ||
|
||
RISC Zero's [zkVM] leverages ZK technology to provide verifiable computation; it does this by proving the correct execution of Rust code. With this proof, anyone can verify that the computation ran correctly and produced the associated outputs. | ||
|
||
For blockchain applications, this proof/verify workflow directly enables strong compute scaling. Expensive computation can be taken offchain, while the proof verification can be done onchain. As long as the proof is valid, the associated contract state can be updated; you do *not* have to trust the party that generates the proof (the prover). | ||
|
||
You can read more at [What does RISC Zero enable]. | ||
|
||
## How much gas is saved? | ||
|
||
The more accounts cast a vote, the more signature verifications ([ecrecover]) are moved from the EVM to RISC Zero's zkVM. | ||
|
||
![Gas Data Comparison](contracts/test/benchmarks/gas_data_comparison.png) | ||
|
||
<p align="center"> | ||
<i>Figure 1: A direct comparison of a test voting workflow in BaselineGovernor (the OpenZeppelin implementation) and RiscZeroGovernor (a modified Governor that utilises offchain compute for signature verification. The relevant test files are located in tests/benchmarks. </i> | ||
</p> | ||
|
||
The x-axis details the number of votes (also the number of accounts, in the testing workflow, each account votes once), and the y-axis the amount of gas spent. This data was generated using [Foundry], specifically its gas reporting and fuzz testing features. Each workflow with a specific number of votes is run 1000 times to provide an average value. | ||
|
||
## What computation is taken offchain? | ||
|
||
The guest program is located at [finalize_votes.rs]. This is the program that runs in the zkVM and a proof of its correct execution is generated. | ||
|
||
At a high level, `finalize_votes.rs`: | ||
- handles signature verification of each vote | ||
- maintains a verifiable hash of all vote data (see `ballotHash` below) | ||
- handles vote updates (only latest vote per address counts) | ||
|
||
## How is this verified onchain? | ||
|
||
[RiscZeroGovernor.sol] has an important function `verifyAndFinalizeVotes`: | ||
|
||
```solidity | ||
function verifyAndFinalizeVotes( | ||
bytes calldata seal, | ||
bytes calldata journal | ||
) public { | ||
// verify the proof | ||
verifier.verify(seal, imageId, sha256(journal)); | ||
// decode the journal | ||
uint256 proposalId = uint256(bytes32(journal[0:32])); | ||
bytes32 ballotHash = bytes32(journal[32:64]); | ||
bytes calldata votingData = journal[64:]; | ||
_finalizeVotes(proposalId, ballotHash, votingData); | ||
} | ||
``` | ||
|
||
This function calls a RISC Zero [verifier contract] to verify the RISC Zero `Groth16Receipt` produced by the prover. If this proof is invalid, the execution will revert within this call, otherwise the [journal] is decoded and the `_finalizeVotes` function within `RiscZeroGovernorCounting.sol` is called. `_finalizeVotes` handles the votes commited to the journal in 100 byte chunks in `votingData`. | ||
|
||
When a user votes, this has both an onchain and an offchain aspect. The vote is processed offchain as seen in [finalize_votes.rs], and onchain `castVote` is called ([RiscZeroGovernor.sol]), which commits the vote by hashing its vote support (a `uint8` representing the vote state, i.e. 1 represents a `for` vote) and the account's address with a hash accumulator of all previous votes. | ||
|
||
This hash accumulator (`ballotHash`) is a commitment that allows offchain voting state to be matched with state onchain; the order of voting will change the final hash and so `ballotHash` is a running representation of voting in an exact order. If an account votes more than once, there is logic to handle only its latest vote as the valid vote, but its data for previous votes is still hashed into `ballotHash`. | ||
|
||
[ecrecover]: https://docs.soliditylang.org/en/latest/cheatsheet.html#index-7 | ||
[finalize_votes.rs]: ./methods/guest/src/bin/finalize_votes.rs | ||
[Foundry]: https://book.getfoundry.sh/ | ||
[Governor]: https://docs.openzeppelin.com/contracts/4.x/governance | ||
[instructions]: ./instructions.md | ||
[journal]: https://dev.risczero.com/terminology#journal | ||
[RiscZeroGovernor.sol]: ./contracts/RiscZeroGovernor.sol | ||
[verifier contract]: https://dev.risczero.com/api/blockchain-integration/contracts/verifier | ||
[What does Risc Zero enable]: https://dev.risczero.com/api/use-cases | ||
[zkVM]: https://dev.risczero.com/zkvm | ||
|
||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
[package] | ||
name = "apps" | ||
version = { workspace = true } | ||
edition = { workspace = true } | ||
|
||
[dependencies] | ||
alloy = { version = "0.2", features = ["full"] } | ||
alloy-primitives = { workspace = true } | ||
alloy-sol-types = { workspace = true } | ||
anyhow = { workspace = true } | ||
clap = { version = "4.5", features = ["derive", "env"] } | ||
env_logger = { version = "0.10" } | ||
governance-methods = { workspace = true } | ||
hex = { workspace = true } | ||
log = { workspace = true } | ||
risc0-ethereum-contracts = { workspace = true } | ||
risc0-zkvm = { workspace = true, features = ["client"] } | ||
tokio = { workspace = true, features = ["full"] } | ||
tracing-subscriber = { workspace = true } | ||
url = { workspace = true } | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# Apps | ||
|
||
In typical applications, an off-chain app is needed to do two main actions: | ||
|
||
* Produce a proof (see [proving options][proving-options]). | ||
* Send a transaction to Ethereum to execute your on-chain logic. | ||
|
||
This template provides the `publisher` CLI as an example application to execute these steps. | ||
In a production application, a back-end server or your dApp client may take on this role. | ||
|
||
## Publisher | ||
|
||
The [`publisher` CLI][publisher], is an example application that produces a proof and publishes it to your app contract. | ||
|
||
### Usage | ||
|
||
Run the `publisher` with: | ||
|
||
```sh | ||
cargo run --bin publisher | ||
``` | ||
|
||
[proving-options]: https://dev.risczero.com/api/generating-proofs/proving-options | ||
[publisher]: ./src/bin/publisher.rs |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
// Copyright 2024 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. | ||
|
||
// This application demonstrates how to send an off-chain proof request | ||
// to the Bonsai proving service and publish the received proofs directly | ||
// to your deployed app contract. | ||
|
||
use anyhow::{Context, Result}; | ||
use clap::Parser; | ||
|
||
use alloy::{ | ||
network::{EthereumWallet, TransactionBuilder}, | ||
primitives::{Address, Bytes}, | ||
providers::{Provider, ProviderBuilder}, | ||
rpc::types::TransactionRequest, | ||
signers::local::PrivateKeySigner, | ||
sol, | ||
// sol_types::SolInterface | ||
}; | ||
|
||
use governance_methods::FINALIZE_VOTES_ELF; | ||
use risc0_ethereum_contracts::encode_seal; | ||
use risc0_zkvm::{default_prover, ExecutorEnv, ProverOpts, VerifierContext}; | ||
// use tokio::task; | ||
use tracing_subscriber::EnvFilter; | ||
use url::Url; | ||
|
||
sol! { | ||
/// ERC-20 balance function signature. | ||
/// This must match the signature in the guest. | ||
interface RiscZeroGovernor { | ||
function verifyAndFinalizeVotes(bytes calldata seal, bytes calldata journal) public; | ||
} | ||
} | ||
|
||
/// Arguments of the publisher CLI. | ||
#[derive(Parser, Debug)] | ||
#[clap(author, version, about, long_about = None)] | ||
struct Args { | ||
/// Ethereum Wallet Private Key | ||
#[clap(long, env)] | ||
eth_wallet_private_key: PrivateKeySigner, | ||
|
||
/// Node RPC URL | ||
#[clap(long)] | ||
rpc_url: Url, | ||
|
||
/// Application's contract address on Ethereum | ||
#[clap(long)] | ||
contract: Address, | ||
|
||
/// The proposal ID (32 bytes, hex-encoded) | ||
#[clap(long)] | ||
proposal_id: Bytes, | ||
|
||
/// The votes data (hex-encoded, multiple of 100 bytes) | ||
#[clap(long)] | ||
votes_data: Bytes, | ||
} | ||
|
||
#[tokio::main] | ||
async fn main() -> Result<()> { | ||
// Initialize tracing. In order to view logs, run `RUST_LOG=info cargo run` | ||
tracing_subscriber::fmt() | ||
.with_env_filter(EnvFilter::from_default_env()) | ||
.init(); | ||
// Parse the command line arguments. | ||
let args = Args::parse(); | ||
|
||
// Create an alloy provider for that private key and URL. | ||
let wallet = EthereumWallet::from(args.eth_wallet_private_key); | ||
let provider = ProviderBuilder::new() | ||
.with_recommended_fillers() | ||
.wallet(wallet) | ||
.on_http(args.rpc_url); | ||
|
||
// Decode the hex-encoded proposal ID and votes data | ||
let proposal_id = hex::decode(&args.proposal_id).context("Failed to decode proposal ID")?; | ||
let votes_data = hex::decode(&args.votes_data).context("Failed to decode votes data")?; | ||
|
||
// Validate input lengths | ||
if proposal_id.len() != 32 { | ||
return Err(anyhow::anyhow!("Proposal ID must be 32 bytes")); | ||
} | ||
if votes_data.len() % 100 != 0 { | ||
return Err(anyhow::anyhow!( | ||
"Votes data must be a multiple of 100 bytes" | ||
)); | ||
} | ||
|
||
// Combine proposal ID and votes data | ||
let input = [&proposal_id[..], &votes_data[..]].concat(); | ||
|
||
let env = ExecutorEnv::builder().write_slice(&input).build()?; | ||
|
||
let receipt = default_prover() | ||
.prove_with_ctx( | ||
env, | ||
&VerifierContext::default(), | ||
FINALIZE_VOTES_ELF, | ||
&ProverOpts::groth16(), | ||
)? | ||
.receipt; | ||
|
||
// Encode the seal with the selector. | ||
let seal = encode_seal(&receipt)?; | ||
|
||
// Extract the journal from the receipt. | ||
let journal = receipt.journal.bytes.clone(); | ||
|
||
// build calldata | ||
let calldata = RiscZeroGovernor::verifyAndFinalizeVotesCall { | ||
seal: seal.into(), | ||
journal: journal.into(), | ||
}; | ||
|
||
// send tx to callback function: verifyAndFinalizeVotes | ||
let contract = args.contract; | ||
let tx = TransactionRequest::default() | ||
.with_to(contract) | ||
.with_call(&calldata); | ||
let tx_hash = provider | ||
.send_transaction(tx) | ||
.await | ||
.context("Failed to send transaction")?; | ||
println!("Transaction sent with hash: {:?}", tx_hash); | ||
|
||
Ok(()) | ||
} |
Oops, something went wrong.