Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WEB3-79: Refactor of Governance Example for RISC Zero zkVM 1.0 #197

Merged
merged 52 commits into from
Sep 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
00c9f0f
copy over governance example from scratch repo
sashaaldrick Aug 5, 2024
fda6c68
remove local deploys
sashaaldrick Aug 5, 2024
70b3145
remove locallly built ImageID/Elf.sol
sashaaldrick Aug 5, 2024
df00f40
remove old reference test
sashaaldrick Aug 5, 2024
e1890f9
ignore automatically built files i.e ImageID/Elf.sol
sashaaldrick Aug 14, 2024
36bcc71
writing main README.md
sashaaldrick Aug 14, 2024
824f5ca
added BenchmarkGovernorsTest.sol: end to end gas benchmarking of full…
sashaaldrick Aug 15, 2024
9170ac3
added a way to generate a lot of signatures progammatically for testing
sashaaldrick Aug 16, 2024
7a145f5
add programmatic baseline workflow with X sigs
sashaaldrick Aug 16, 2024
d33c1f8
clean up benchmarks with fuzzing and base test contract for utils/state
sashaaldrick Aug 17, 2024
f45dd20
sorting out logging
sashaaldrick Aug 17, 2024
777e178
add gas benchmarking data and python printer
sashaaldrick Aug 17, 2024
9922ee7
cleaned up original tests with Base and added benchmarks folder for c…
sashaaldrick Aug 17, 2024
3c64b80
added gas benchmaks png file
sashaaldrick Aug 17, 2024
44bc8a6
fix spdx licenses
sashaaldrick Aug 17, 2024
55ccc0c
destructure without assigning unsigned variables in one test
sashaaldrick Aug 17, 2024
ab3cb16
add image to README
sashaaldrick Aug 17, 2024
24aa75a
TODO: finish README
sashaaldrick Aug 17, 2024
a5ccf78
complete README and add instructions.md to reproduce data with python…
sashaaldrick Aug 18, 2024
bdfa419
fix issue with writing gas data to root instead of tests/benchmarks
sashaaldrick Aug 18, 2024
6df2d9c
Merge branch 'main' into sasha/governance-example
sashaaldrick Aug 19, 2024
9d92c9f
redirected forge-std and openzeppelin contracts to correct lib folder…
sashaaldrick Aug 19, 2024
0641659
fixed one stage of import issues, using root lib for most
sashaaldrick Aug 19, 2024
02bf1b6
removed duplicate contracts
sashaaldrick Aug 19, 2024
2bf0860
added licenses to satisfy license-check
sashaaldrick Aug 19, 2024
369c325
update clap from 4.0 to 4.5 to (???) fix linux CI
sashaaldrick Aug 19, 2024
246ed6c
trying updating clap to workspace true
sashaaldrick Aug 19, 2024
4eadcff
update main.yml workflow to include 'cargo build' in governance
sashaaldrick Aug 19, 2024
13b7047
angelo fixes for build
sashaaldrick Aug 19, 2024
021271c
generate_proposal_id.rs working
sashaaldrick Sep 1, 2024
dc4e939
add generate_proposal_id, generate_vote_data & victor's suggestions
sashaaldrick Sep 4, 2024
92f7d26
pull new changes
sashaaldrick Sep 4, 2024
bfde192
add testing to CI, add licenses, fix build error
sashaaldrick Sep 4, 2024
009534c
vote data readme changes
sashaaldrick Sep 5, 2024
1fea86f
Merge branch 'main' into sasha/governance-example
sashaaldrick Sep 5, 2024
e323629
fix code snippets on apps readme
sashaaldrick Sep 5, 2024
2c457f2
Suggestions from review of #197 (#226)
nategraf Sep 9, 2024
0fdf00b
all solidity + readme fixes from review added
sashaaldrick Sep 9, 2024
1e83ba9
remove cached tracked files, imageID and gas_data
sashaaldrick Sep 9, 2024
e901886
remove generation scripts, to be added back in future PR
sashaaldrick Sep 9, 2024
3ae7cf1
add ECDSA acceleration with patched crates
sashaaldrick Sep 10, 2024
087710d
Merge branch 'main' into sasha/governance-example
sashaaldrick Sep 10, 2024
ca7efbb
refactored contracts folder structure to match other examples for con…
sashaaldrick Sep 11, 2024
0665d80
use generic examples script in git workflow
sashaaldrick Sep 11, 2024
396e6c8
cargo fmt ran to fix CI
sashaaldrick Sep 11, 2024
01c4ea7
run cargo sort checks
sashaaldrick Sep 11, 2024
78558ff
pls last ci check fail
sashaaldrick Sep 11, 2024
e3b3a98
add Victor review changes 12.09
sashaaldrick Sep 12, 2024
3e16a14
Merge branch 'main' into sasha/governance-example
sashaaldrick Sep 12, 2024
653c9c5
fix broken links in instructions.md
sashaaldrick Sep 12, 2024
64a8850
image fix in instructions.md
sashaaldrick Sep 12, 2024
e010adb
Merge branch 'main' into sasha/governance-example
sashaaldrick Sep 14, 2024
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
26 changes: 26 additions & 0 deletions examples/governance/.gitignore
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
35 changes: 35 additions & 0 deletions examples/governance/Cargo.toml
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
78 changes: 78 additions & 0 deletions examples/governance/README.md
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




21 changes: 21 additions & 0 deletions examples/governance/apps/Cargo.toml
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 }

24 changes: 24 additions & 0 deletions examples/governance/apps/README.md
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
140 changes: 140 additions & 0 deletions examples/governance/apps/src/bin/publisher.rs
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(())
}
Loading
Loading