Skip to content

Commit

Permalink
fix(l2): replace ExecutionDB from_exec() impl to use a CacheDB
Browse files Browse the repository at this point in the history
…based approach (#1709)

**Motivation**

A `ExecutionDB` is created from a block's pre-execution but the current
implementation is incorrect and not all needed data gets stored. The
goal is to replace this with another approach, using an auxiliary
"caching" database that will retrieve data from a `Store` or an RPC
endpoint (like in #1705) during the pre-execution of a block, finally
resulting in a db that contains all the needed execution data.

**Description**

- replace `ExecutionDB::from_exec()` to use the `CacheDB`, rename it to
`from_store()`
- some refactors
  • Loading branch information
xqft authored Jan 24, 2025
1 parent b2a304e commit 95b653c
Show file tree
Hide file tree
Showing 11 changed files with 330 additions and 176 deletions.
3 changes: 2 additions & 1 deletion crates/l2/proposer/prover_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,8 @@ impl ProverServer {

let block = Block::new(header, body);

let db = ExecutionDB::from_exec(&block, &self.store).map_err(EvmError::ExecutionDB)?;
let db =
ExecutionDB::from_store(&block, self.store.clone()).map_err(EvmError::ExecutionDB)?;

let parent_block_header = self
.store
Expand Down
2 changes: 1 addition & 1 deletion crates/l2/prover/tests/perf_zkvm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ async fn setup() -> (ProgramInput, Block) {
}
let block_to_prove = blocks.last().unwrap();

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

let parent_block_header = store
.get_block_header_by_hash(block_to_prove.header.parent_hash)
Expand Down
16 changes: 10 additions & 6 deletions crates/l2/prover/zkvm/interface/risc0/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use ethrex_blockchain::{validate_block, validate_gas_used};
use ethrex_vm::{execute_block, get_state_transitions, EvmState};
use zkvm_interface::{
io::{ProgramInput, ProgramOutput},
trie::update_tries,
trie::{update_tries, verify_db},
};

fn main() {
Expand All @@ -15,36 +15,40 @@ fn main() {
} = env::read();
let mut state = EvmState::from(db.clone());

// Validate the block pre-execution
// Validate the block
validate_block(&block, &parent_block_header, &state).expect("invalid block");

// Validate the initial state
// Tries used for validating initial and final state root
let (mut state_trie, mut storage_tries) = db
.build_tries()
.get_tries()
.expect("failed to build state and storage tries or state is not valid");

// Validate the initial state
let initial_state_hash = state_trie.hash_no_commit();
if initial_state_hash != parent_block_header.state_root {
panic!("invalid initial state trie");
}
if !verify_db(&db, &state_trie, &storage_tries).expect("failed to validate database") {
panic!("invalid database")
};

let receipts = execute_block(&block, &mut state).expect("failed to execute block");
validate_gas_used(&receipts, &block.header).expect("invalid gas used");

// Output gas for measurement purposes
let cumulative_gas_used = receipts
.last()
.map(|last_receipt| last_receipt.cumulative_gas_used)
.unwrap_or_default();

env::write(&cumulative_gas_used);

let account_updates = get_state_transitions(&mut state);

// Update tries and calculate final state root hash
update_tries(&mut state_trie, &mut storage_tries, &account_updates)
.expect("failed to update state and storage tries");
let final_state_hash = state_trie.hash_no_commit();

let final_state_hash = state_trie.hash_no_commit();
if final_state_hash != block.header.state_root {
panic!("invalid final state trie");
}
Expand Down
Binary file not shown.
17 changes: 10 additions & 7 deletions crates/l2/prover/zkvm/interface/sp1/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use ethrex_blockchain::{validate_block, validate_gas_used};
use ethrex_vm::{execute_block, get_state_transitions, EvmState};
use zkvm_interface::{
io::{ProgramInput, ProgramOutput},
trie::update_tries,
trie::{update_tries, verify_db},
};

sp1_zkvm::entrypoint!(main);
Expand All @@ -15,39 +15,42 @@ pub fn main() {
parent_block_header,
db,
} = sp1_zkvm::io::read::<ProgramInput>();

let mut state = EvmState::from(db.clone());

// Validate the block pre-execution
// Validate the block
validate_block(&block, &parent_block_header, &state).expect("invalid block");

// Validate the initial state
// Tries used for validating initial and final state root
let (mut state_trie, mut storage_tries) = db
.build_tries()
.get_tries()
.expect("failed to build state and storage tries or state is not valid");

// Validate the initial state
let initial_state_hash = state_trie.hash_no_commit();
if initial_state_hash != parent_block_header.state_root {
panic!("invalid initial state trie");
}
if !verify_db(&db, &state_trie, &storage_tries).expect("failed to validate database") {
panic!("invalid database")
};

let receipts = execute_block(&block, &mut state).expect("failed to execute block");
validate_gas_used(&receipts, &block.header).expect("invalid gas used");

// Output gas for measurement purposes
let cumulative_gas_used = receipts
.last()
.map(|last_receipt| last_receipt.cumulative_gas_used)
.unwrap_or_default();

sp1_zkvm::io::commit(&cumulative_gas_used);

let account_updates = get_state_transitions(&mut state);

// Update tries and calculate final state root hash
update_tries(&mut state_trie, &mut storage_tries, &account_updates)
.expect("failed to update state and storage tries");
let final_state_hash = state_trie.hash_no_commit();

let final_state_hash = state_trie.hash_no_commit();
if final_state_hash != block.header.state_root {
panic!("invalid final state trie");
}
Expand Down
54 changes: 52 additions & 2 deletions crates/l2/prover/zkvm/interface/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,11 @@ pub mod io {
pub mod trie {
use std::collections::HashMap;

use ethrex_core::{types::AccountState, H160};
use ethrex_core::{types::AccountState, H160, U256};
use ethrex_rlp::{decode::RLPDecode, encode::RLPEncode, error::RLPDecodeError};
use ethrex_storage::{hash_address, hash_key, AccountUpdate};
use ethrex_trie::{Trie, TrieError};
use ethrex_vm::execution_db::ExecutionDB;
use thiserror::Error;

#[derive(Debug, Error)]
Expand All @@ -91,9 +92,58 @@ pub mod trie {
#[error(transparent)]
RLPDecode(#[from] RLPDecodeError),
#[error("Missing storage trie for address {0}")]
MissingStorageTrie(H160),
#[error("Missing storage for address {0}")]
StorageNotFound(H160),
}

pub fn verify_db(
db: &ExecutionDB,
state_trie: &Trie,
storage_tries: &HashMap<H160, Trie>,
) -> Result<bool, Error> {
for (address, account_info) in &db.accounts {
let storage_trie = storage_tries
.get(address)
.ok_or(Error::MissingStorageTrie(*address))?;
let storage_root = storage_trie.hash_no_commit();

// verify account and storage trie are valid
let trie_account_state = match state_trie.get(&hash_address(address)) {
Ok(Some(encoded_state)) => AccountState::decode(&encoded_state)?,
Ok(None) | Err(TrieError::InconsistentTree) => return Ok(false), // account not in trie
Err(err) => return Err(err.into()),
};
let db_account_state = AccountState {
nonce: account_info.nonce,
balance: account_info.balance,
code_hash: account_info.code_hash,
storage_root,
};
if db_account_state != trie_account_state {
return Ok(false);
}

// verify storage
for (key, db_value) in db
.storage
.get(address)
.ok_or(Error::StorageNotFound(*address))?
{
let trie_value = match storage_trie.get(&hash_key(key)) {
Ok(Some(encoded)) => U256::decode(&encoded)?,
Ok(None) | Err(TrieError::InconsistentTree) => return Ok(false), // value not in trie
Err(err) => return Err(err.into()),
};
if *db_value != trie_value {
return Ok(false);
}
}
}

Ok(true)
}

pub fn update_tries(
state_trie: &mut Trie,
storage_tries: &mut HashMap<H160, Trie>,
Expand Down Expand Up @@ -133,7 +183,7 @@ pub mod trie {
} else {
storage_tries
.get_mut(&update.address)
.ok_or(Error::StorageNotFound(update.address))?
.ok_or(Error::MissingStorageTrie(update.address))?
};
for (storage_key, storage_value) in &update.added_storage {
let hashed_key = hash_key(storage_key);
Expand Down
2 changes: 1 addition & 1 deletion crates/l2/utils/test_data_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ pub fn generate_program_input(
.ok_or(ProverInputError::InvalidParentBlock(
block.header.parent_hash,
))?;
let db = ExecutionDB::from_exec(&block, &store)?;
let db = ExecutionDB::from_store(&block, store)?;

Ok(ProgramInput {
db,
Expand Down
51 changes: 51 additions & 0 deletions crates/vm/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,54 @@ impl revm::Database for StoreWrapper {
.ok_or_else(|| StoreError::Custom(format!("Block {number} not found")))
}
}

impl revm::DatabaseRef for StoreWrapper {
type Error = StoreError;

fn basic_ref(&self, address: RevmAddress) -> Result<Option<RevmAccountInfo>, Self::Error> {
let acc_info = match self
.store
.get_account_info_by_hash(self.block_hash, CoreAddress::from(address.0.as_ref()))?
{
None => return Ok(None),
Some(acc_info) => acc_info,
};
let code = self
.store
.get_account_code(acc_info.code_hash)?
.map(|b| RevmBytecode::new_raw(RevmBytes(b)));

Ok(Some(RevmAccountInfo {
balance: RevmU256::from_limbs(acc_info.balance.0),
nonce: acc_info.nonce,
code_hash: RevmB256::from(acc_info.code_hash.0),
code,
}))
}

fn code_by_hash_ref(&self, code_hash: RevmB256) -> Result<RevmBytecode, Self::Error> {
self.store
.get_account_code(CoreH256::from(code_hash.as_ref()))?
.map(|b| RevmBytecode::new_raw(RevmBytes(b)))
.ok_or_else(|| StoreError::Custom(format!("No code for hash {code_hash}")))
}

fn storage_ref(&self, address: RevmAddress, index: RevmU256) -> Result<RevmU256, Self::Error> {
Ok(self
.store
.get_storage_at_hash(
self.block_hash,
CoreAddress::from(address.0.as_ref()),
CoreH256::from(index.to_be_bytes()),
)?
.map(|value| RevmU256::from_limbs(value.0))
.unwrap_or_else(|| RevmU256::ZERO))
}

fn block_hash_ref(&self, number: u64) -> Result<RevmB256, Self::Error> {
self.store
.get_block_header(number)?
.map(|header| RevmB256::from_slice(&header.compute_block_hash().0))
.ok_or_else(|| StoreError::Custom(format!("Block {number} not found")))
}
}
12 changes: 8 additions & 4 deletions crates/vm/errors.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use ethereum_types::{H160, H256};
use ethrex_core::types::BlockHash;
use ethrex_core::{types::BlockHash, Address};
use ethrex_storage::error::StoreError;
use ethrex_trie::TrieError;
use revm::primitives::{
Expand Down Expand Up @@ -43,14 +43,18 @@ pub enum ExecutionDBError {
StorageValueNotFound(RevmAddress, RevmU256),
#[error("Hash of block with number {0} not found")]
BlockHashNotFound(u64),
#[error("Missing account {0} info while trying to create ExecutionDB")]
NewMissingAccountInfo(RevmAddress),
#[error("Missing state trie of block {0} while trying to create ExecutionDB")]
NewMissingStateTrie(BlockHash),
#[error(
"Missing storage trie of block {0} and address {1} while trying to create ExecutionDB"
)]
NewMissingStorageTrie(BlockHash, H160),
NewMissingStorageTrie(BlockHash, Address),
#[error("Missing account {0} info while trying to create ExecutionDB")]
NewMissingAccountInfo(Address),
#[error("Missing storage of address {0} and key {1} while trying to create ExecutionDB")]
NewMissingStorage(Address, H256),
#[error("Missing code of hash {0} while trying to create ExecutionDB")]
NewMissingCode(H256),
#[error("The account {0} is not included in the stored pruned state trie")]
MissingAccountInStateTrie(H160),
#[error("Missing storage trie of account {0}")]
Expand Down
Loading

0 comments on commit 95b653c

Please sign in to comment.