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

feat(l2): initial state pruned trie in zkVM program #1133

Merged
merged 48 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
2e8bede
remove zkvm_interface unused dependencies
xqft Nov 11, 2024
e3a78a3
introduce ProgramInput and ProgramOutput
xqft Nov 11, 2024
2022118
remove zkvm_interface reexport
xqft Nov 11, 2024
6180bc6
Merge branch 'main' into l2/zkvm_trie
xqft Nov 12, 2024
e7f2232
use RLP to serialize block
xqft Nov 13, 2024
ba2e5e4
nit deps
xqft Nov 13, 2024
0bdf429
Merge branch 'main' into l2/zkvm_trie
xqft Nov 13, 2024
db89d22
add trie null db
xqft Nov 13, 2024
2226799
add get_pruned_state() and from_nodes()
xqft Nov 13, 2024
2560f05
add hash_no_commit() fn
xqft Nov 14, 2024
d1c9c53
return root state separately
xqft Nov 14, 2024
259c7bd
get pruned state at executiondb creation
xqft Nov 14, 2024
1fe4fcc
integrate with zkvm
xqft Nov 14, 2024
fdf220b
support empty tries
xqft Nov 14, 2024
b14e8c0
remove dbg
xqft Nov 14, 2024
58e3309
add update_tries() and commit program output
xqft Nov 15, 2024
99854d5
fix get_transitions()
xqft Nov 15, 2024
804ef55
Merge branch 'main' into l2/zkvm_trie
xqft Nov 15, 2024
7e6b8e2
Merge branch 'l1/fix_executiondb_updates' into l2/zkvm_trie
xqft Nov 15, 2024
fbc8a76
fix test
xqft Nov 18, 2024
51a2e58
make ci job run on every case
xqft Nov 18, 2024
5f32ce5
Merge branch 'l2/fix_zkvm_perf_test' into l2/zkvm_trie
xqft Nov 18, 2024
5c58ea8
change zkvm log to info:
xqft Nov 18, 2024
20c44d3
check final trie hash
xqft Nov 18, 2024
e3253dc
update expect comment
xqft Nov 18, 2024
62f457a
remove todo
xqft Nov 19, 2024
6958242
fix trie error
xqft Nov 19, 2024
11d178f
remove cumulative gas as output
xqft Nov 19, 2024
3ad76a8
add execution program documentation
xqft Nov 19, 2024
b86c0de
Merge branch 'main' into l2/zkvm_trie
xqft Nov 19, 2024
721b565
fix doc identation
xqft Nov 20, 2024
b5b48c4
Merge branch 'main' into l2/zkvm_trie
xqft Nov 20, 2024
d477134
reimplement get_pruned_state
xqft Nov 20, 2024
7ed72e2
rename fn
xqft Nov 20, 2024
dc0adf3
fix zkvm program imports
xqft Nov 20, 2024
9c066da
Merge branch 'main' into l2/zkvm_trie
xqft Nov 20, 2024
5849477
fix perf_zkvm warnings
xqft Nov 20, 2024
39149f0
fix root encoding
xqft Nov 20, 2024
b396d22
fix lint
xqft Nov 20, 2024
aeae84e
disable beacon root call in l2 mode
xqft Nov 22, 2024
270338d
add l2 feature to vm dep in prover
xqft Nov 22, 2024
faf8d8e
add feature to dev dep
xqft Nov 22, 2024
22004c5
fix dep path
xqft Nov 22, 2024
36ea2f3
fix clippy
xqft Nov 22, 2024
041e2a6
Revert "fix clippy"
xqft Nov 22, 2024
d20df52
Revert l2 feature changes
xqft Nov 22, 2024
43d807b
disable beacon call root in l2
xqft Nov 22, 2024
aea8462
disable for levm too
xqft Nov 22, 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
3 changes: 2 additions & 1 deletion crates/l2/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ For a high level overview of the L2:

For more detailed documentation on each part of the system:

- [Contracts](./contracts.md)
- [Execution program](./program.md)
- [Proposer](./proposer.md)
- [Prover](./prover.md)
- [Contracts](./contracts.md)

## Configuration

Expand Down
43 changes: 43 additions & 0 deletions crates/l2/docs/program.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Prover's block execution program

The zkVM block execution program will:
1. Take as input:
- the block to verify and its parent's header
- the L2 initial state, stored in a `ExecutionDB` struct, including the nodes for state and storage [pruned tries](#pruned-tries)
1. Build the initial state tries. This includes:
- verifying that the initial state values stored in the `ExecutionDB` are included in the tries.
- checking that the state trie root hash is the same as the one in the parent's header
- building the trie structures
1. Execute the block
1. Perform validations before and after execution
1. Apply account updates to the tries and compute the new state root
1. Check that the final state root is the same as the one stored in the block's header
1. Commit the program's output

## Public and private inputs
The program interface defines a `ProgramInput` and `ProgramOutput` structures.

`ProgramInput` contains:
- the block to verify and its parent's header
- an `ExecutionDB` which only holds the relevant initial state data for executing the block. This is built from pre-executing the block outside the zkVM to get the resulting account updates and retrieving the accounts and storage values touched by the execution.
- the `ExecutionDB` will also include all the (encoded) nodes necessary to build [pruned tries](#pruned-tries) for the stored accounts and storage values.

`ProgramOutput` contains:
- the initial state hash
- the final state hash
these outputs will be committed as part of the proof. Both hashes are verified by the program, with the initial hash being checked at the time of building the initial tries (equivalent to verifying inclusion proofs) and the final hash by applying the account updates (that resulted from the block's execution) in the tries and recomputing the state root.

## Pruned Tries
The EVM state is stored in Merkle Patricia Tries, which work differently than standard Merkle binary trees. In particular we have a *state trie* for each block, which contains all account states, and then for each account we have a *storage trie* that contains every storage value if the account in question corresponds to a deployed smart contract.

We need a way to check the integrity of the account and storage values we pass as input to the block execution program. The "Merkle" in Merkle Patricia Tries means that we can cryptographically check inclusion of any value in a trie, and then use the trie's root to check the integrity of the whole data at once.

Particularly, the root node points to its child nodes by storing their hashes, and these also contain the hashes of *their* child nodes, and so and so, until arriving at nodes that contain the values themselves. This means that the root contains the information of the whole trie (which can be compressed in a single word (32 byte value) by hashing the root), and by traversing down the trie we are checking nodes with more specific information until arriving to some value.

So if we store only the necessary nodes that make up a path from the root into a particular value of interest (including the latter and the former), then:
- we know the root hash of this trie
- we know that this trie includes the value we're interested in
thereby **we're storing a proof of inclusion of the value in a trie with some root hash we can check*, which is equivalent to having a "pruned trie" that only contains the path of interest, but contains information of all other non included nodes and paths (subtries) thanks to nodes storing their childs hashes as mentioned earlier. This way we can verify the inclusion of values in some state, and thus the validity of the initial state values in the `ExecutionDB`, because we know the correct root hash.

We can mutate this pruned trie by modifying/removing some value or inserting a new one, and then recalculate all the hashes from the node we inserted/modified up to the root, finally computing the new root hash. Because we know the correct final state root hash, this way we can make sure that the execution lead to the correct final state values.

8 changes: 4 additions & 4 deletions crates/l2/proposer/prover_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ use risc0_zkvm::sha::{Digest, Digestible};

#[derive(Debug, Serialize, Deserialize, Default)]
pub struct ProverInputData {
pub db: ExecutionDB,
pub block: Block,
pub parent_header: BlockHeader,
pub parent_block_header: BlockHeader,
pub db: ExecutionDB,
}

use crate::utils::{
Expand Down Expand Up @@ -408,7 +408,7 @@ impl ProverServer {

let db = ExecutionDB::from_exec(&block, &self.store).map_err(|err| err.to_string())?;

let parent_header = self
let parent_block_header = self
.store
.get_block_header_by_hash(block.header.parent_hash)
.map_err(|err| err.to_string())?
Expand All @@ -419,7 +419,7 @@ impl ProverServer {
Ok(ProverInputData {
db,
block,
parent_header,
parent_block_header,
})
}

Expand Down
2 changes: 1 addition & 1 deletion crates/l2/prover/Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
RISC0_DEV_MODE?=1
RUST_LOG?="debug"
RUST_LOG?="info"
perf_test_proving:
@echo "Using RISC0_DEV_MODE: ${RISC0_DEV_MODE}"
RISC0_DEV_MODE=${RISC0_DEV_MODE} RUST_LOG=${RUST_LOG} cargo test --release --test perf_zkvm --features build_zkvm -- --show-output
Expand Down
63 changes: 19 additions & 44 deletions crates/l2/prover/src/prover.rs
Original file line number Diff line number Diff line change
@@ -1,34 +1,19 @@
use serde::Deserialize;
use tracing::info;

// risc0
use zkvm_interface::methods::{ZKVM_PROGRAM_ELF, ZKVM_PROGRAM_ID};

use risc0_zkvm::{default_prover, ExecutorEnv, ExecutorEnvBuilder, ProverOpts};

use ethrex_core::types::Receipt;
use ethrex_l2::{
proposer::prover_server::ProverInputData, utils::config::prover_client::ProverClientConfig,
use zkvm_interface::{
io::{ProgramInput, ProgramOutput},
methods::{ZKVM_PROGRAM_ELF, ZKVM_PROGRAM_ID},
};
use ethrex_rlp::encode::RLPEncode;
use ethrex_vm::execution_db::ExecutionDB;

// The order of variables in this structure should match the order in which they were
// committed in the zkVM, with each variable represented by a field.
#[derive(Debug, Deserialize)]
pub struct ProverOutputData {
/// It is rlp encoded, it has to be decoded.
/// Block::decode(&prover_output_data.block).unwrap());
pub _block: Vec<u8>,
pub _execution_db: ExecutionDB,
pub _parent_block_header: Vec<u8>,
pub block_receipts: Vec<Receipt>,
}
use risc0_zkvm::{default_prover, ExecutorEnv, ProverOpts};

use ethrex_l2::utils::config::prover_client::ProverClientConfig;

pub struct Prover<'a> {
env_builder: ExecutorEnvBuilder<'a>,
elf: &'a [u8],
pub id: [u32; 8],
pub stdout: Vec<u8>,
}

impl<'a> Default for Prover<'a> {
Expand All @@ -41,29 +26,20 @@ impl<'a> Default for Prover<'a> {
impl<'a> Prover<'a> {
pub fn new() -> Self {
Self {
env_builder: ExecutorEnv::builder(),
elf: ZKVM_PROGRAM_ELF,
id: ZKVM_PROGRAM_ID,
stdout: Vec::new(),
}
}

pub fn set_input(&mut self, input: ProverInputData) -> &mut Self {
let head_block_rlp = input.block.encode_to_vec();
let parent_header_rlp = input.parent_header.encode_to_vec();

// We should pass the inputs as a whole struct
self.env_builder.write(&head_block_rlp).unwrap();
self.env_builder.write(&input.db).unwrap();
self.env_builder.write(&parent_header_rlp).unwrap();

self
}

/// Example:
/// let prover = Prover::new();
/// let proof = prover.set_input(inputs).prove().unwrap();
pub fn prove(&mut self) -> Result<risc0_zkvm::Receipt, Box<dyn std::error::Error>> {
let env = self.env_builder.build()?;
pub fn prove(
&mut self,
input: ProgramInput,
) -> Result<risc0_zkvm::Receipt, Box<dyn std::error::Error>> {
let env = ExecutorEnv::builder()
.stdout(&mut self.stdout)
.write(&input)?
.build()?;

// Generate the Receipt
let prover = default_prover();
Expand All @@ -72,7 +48,7 @@ impl<'a> Prover<'a> {
// This struct contains the receipt along with statistics about execution of the guest
let prove_info = prover.prove_with_opts(env, self.elf, &ProverOpts::groth16())?;

// extract the receipt.
// Extract the receipt.
let receipt = prove_info.receipt;

info!("Successfully generated execution receipt.");
Expand All @@ -87,8 +63,7 @@ impl<'a> Prover<'a> {

pub fn get_commitment(
receipt: &risc0_zkvm::Receipt,
) -> Result<ProverOutputData, Box<dyn std::error::Error>> {
let commitment: ProverOutputData = receipt.journal.decode()?;
Ok(commitment)
) -> Result<ProgramOutput, Box<dyn std::error::Error>> {
Ok(receipt.journal.decode()?)
}
}
15 changes: 10 additions & 5 deletions crates/l2/prover/src/prover_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ use std::{
use tokio::time::sleep;
use tracing::{debug, error, info, warn};

use zkvm_interface::io::ProgramInput;

use ethrex_l2::{
proposer::prover_server::{ProofData, ProverInputData},
utils::config::prover_client::ProverClientConfig,
proposer::prover_server::ProofData, utils::config::prover_client::ProverClientConfig,
};

use super::prover::Prover;
Expand Down Expand Up @@ -38,7 +39,7 @@ impl ProverClient {
loop {
match self.request_new_input() {
Ok((block_number, input)) => {
match prover.set_input(input).prove() {
match prover.prove(input) {
Ok(proof) => {
if let Err(e) =
self.submit_proof(block_number, proof, prover.id.to_vec())
Expand All @@ -58,7 +59,7 @@ impl ProverClient {
}
}

fn request_new_input(&self) -> Result<(u64, ProverInputData), String> {
fn request_new_input(&self) -> Result<(u64, ProgramInput), String> {
// Request the input with the correct block_number
let request = ProofData::Request;
let response = connect_to_prover_server_wr(&self.prover_server_endpoint, &request)
Expand All @@ -71,7 +72,11 @@ impl ProverClient {
} => match (block_number, input) {
(Some(n), Some(i)) => {
info!("Received Response for block_number: {n}");
Ok((n, i))
Ok((n, ProgramInput {
block: i.block,
parent_block_header: i.parent_block_header,
db: i.db
}))
}
_ => Err(
"Received Empty Response, meaning that the ProverServer doesn't have blocks to prove.\nThe Prover may be advancing faster than the Proposer."
Expand Down
24 changes: 8 additions & 16 deletions crates/l2/prover/tests/perf_zkvm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ use std::path::Path;
use tracing::info;

use ethrex_blockchain::add_block;
use ethrex_l2::proposer::prover_server::ProverInputData;
use ethrex_prover_lib::prover::Prover;
use ethrex_storage::{EngineType, Store};
use ethrex_vm::execution_db::ExecutionDB;
use zkvm_interface::io::ProgramInput;

#[tokio::test]
async fn test_performance_zkvm() {
tracing_subscriber::fmt::init();

let mut path = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../../../test_data"));
let path = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../../../test_data"));

// Another use is genesis-execution-api.json in conjunction with chain.rlp(20 blocks not too loaded).
let genesis_file_path = path.join("genesis-l2-old.json");
Expand All @@ -34,23 +34,22 @@ async fn test_performance_zkvm() {

let db = ExecutionDB::from_exec(block_to_prove, &store).unwrap();

let parent_header = store
let parent_block_header = store
.get_block_header_by_hash(block_to_prove.header.parent_hash)
.unwrap()
.unwrap();

let input = ProverInputData {
db,
let input = ProgramInput {
block: block_to_prove.clone(),
parent_header,
parent_block_header,
db,
};

let mut prover = Prover::new();
prover.set_input(input);

let start = std::time::Instant::now();

let receipt = prover.prove().unwrap();
let receipt = prover.prove(input).unwrap();

let duration = start.elapsed();
info!(
Expand All @@ -62,12 +61,5 @@ async fn test_performance_zkvm() {

prover.verify(&receipt).unwrap();

let output = Prover::get_commitment(&receipt).unwrap();

let execution_cumulative_gas_used = output.block_receipts.last().unwrap().cumulative_gas_used;
info!("Cumulative Gas Used {execution_cumulative_gas_used}");

let gas_per_second = execution_cumulative_gas_used as f64 / duration.as_secs_f64();

info!("Gas per Second: {}", gas_per_second);
let _program_output = Prover::get_commitment(&receipt).unwrap();
}
18 changes: 8 additions & 10 deletions crates/l2/prover/zkvm/interface/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,15 @@ version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1.0", default-features = false, features = ["derive"] }
thiserror = "1.0.64"
serde = { version = "1.0.203", features = ["derive"] }
serde_with = "3.11.0"
thiserror = "1.0.61"

ethrex-storage = { path = "../../../../storage/store" }

# revm
revm = { version = "14.0.3", features = [
"std",
"serde",
"kzg-rs",
], default-features = false }
ethrex-core = { path = "../../../../common/", default-features = false }
ethrex-vm = { path = "../../../../vm", default-features = false }
ethrex-rlp = { path = "../../../../common/rlp", default-features = false }
ethrex-storage = { path = "../../../../storage/store", default-features = false }
ethrex-trie = { path = "../../../../storage/trie", default-features = false }

[build-dependencies]
risc0-build = { version = "1.1.2" }
Expand Down
5 changes: 4 additions & 1 deletion crates/l2/prover/zkvm/interface/guest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ edition = "2021"

[dependencies]
risc0-zkvm = { version = "1.1.2", default-features = false, features = ["std"] }
zkvm_interface = { path = "../" }

ethrex-core = { path = "../../../../../common", default-features = false }
ethrex-rlp = { path = "../../../../../common/rlp" }
ethrex-vm = { path = "../../../../../vm", default-features = false }
ethrex-vm = { path = "../../../../../vm", default-features = false, features = [
"l2",
] }
ethrex-blockchain = { path = "../../../../../blockchain", default-features = false }

[build-dependencies]
Expand Down
Loading
Loading