Skip to content

refactor(levm): implement cache rollback #2417

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

Merged
merged 59 commits into from
Apr 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
f7f756d
first ugly implementation of cache rollback
JereSalo Apr 7, 2025
ea6d4b4
change remove_account
JereSalo Apr 7, 2025
8f11f8a
change benchmark stateless_execute for execute()
JereSalo Apr 7, 2025
5cc6827
quick removal of cache_backup
JereSalo Apr 7, 2025
4094cc7
fix prepare execution
JereSalo Apr 7, 2025
13dc11b
clippy lints
JereSalo Apr 7, 2025
3c136f1
create cache restore method
JereSalo Apr 7, 2025
aaa988b
fix errors
JereSalo Apr 8, 2025
caf5155
move insert_account
JereSalo Apr 8, 2025
e5e46fa
remove option from insert_account
JereSalo Apr 8, 2025
7b6c4be
move remove_account
JereSalo Apr 8, 2025
62fda8e
make things more tidy
JereSalo Apr 8, 2025
aabbdaa
run cargo fmt
JereSalo Apr 8, 2025
b992149
minimal changes
JereSalo Apr 8, 2025
1fe19c7
make database interactions be methods of GeneralizedDatabase
JereSalo Apr 8, 2025
642071d
make callframe immutable in finalize_execution
JereSalo Apr 8, 2025
bffe6d0
Merge branch 'levm/move_methods_to_db' into levm/cache_rollback
JereSalo Apr 8, 2025
e9564d9
change option to be immutable
JereSalo Apr 8, 2025
792ecbb
add gen_db
JereSalo Apr 8, 2025
ab4949c
run cargo fmt
JereSalo Apr 8, 2025
191dc3d
rename get_account_mut_vm to get_account_mut
JereSalo Apr 8, 2025
05efe20
add some comments
JereSalo Apr 8, 2025
640e041
remove comment from get account mut
JereSalo Apr 9, 2025
ec906ec
cargo fmt
JereSalo Apr 9, 2025
1691105
clean code
JereSalo Apr 9, 2025
c8c560b
add comments
JereSalo Apr 9, 2025
8b91321
empty commit
JereSalo Apr 10, 2025
6873459
Merge branch 'main' into levm/cache_rollback
JereSalo Apr 10, 2025
2cc41ba
improve restore state arguments
JereSalo Apr 11, 2025
ce096c0
add comment claryfing clearing cache backup
JereSalo Apr 11, 2025
a4352b0
clippy lint
JereSalo Apr 11, 2025
914aba5
Merge branch 'main' into levm/cache_rollback
JereSalo Apr 11, 2025
e01a9f6
Merge branch 'main' into levm/cache_rollback
JereSalo Apr 11, 2025
08443e3
starting with fix after merge
JereSalo Apr 11, 2025
ea5ea56
fix
JereSalo Apr 12, 2025
ad40095
delete comments
JereSalo Apr 12, 2025
3ba475a
replace CallFrame for CacheBackup in some methods
JereSalo Apr 14, 2025
c096368
make callframe immutable in finalize_execution
JereSalo Apr 14, 2025
bac54a0
remove field account (used for testing) from vm
JereSalo Apr 14, 2025
959becf
add comment
JereSalo Apr 14, 2025
37c285a
remove coinbase fee check diff than zero
JereSalo Apr 14, 2025
c9f02a3
Merge branch 'main' into levm/cache_rollback
JereSalo Apr 14, 2025
212aa4f
Merge branch 'main' into levm/cache_rollback
jrchatruc Apr 15, 2025
a453f3c
fix merge main
tomip01 Apr 15, 2025
27ae20b
fix import
tomip01 Apr 15, 2025
d46a1fe
refactor(levm): merge main to cache_rollback (#2557)
JereSalo Apr 24, 2025
e9b5add
merge main
JereSalo Apr 24, 2025
1f84ba9
clippy
JereSalo Apr 24, 2025
555669a
move some methods to utils.rs to declutter vm.rs
JereSalo Apr 24, 2025
acf7d82
cargo fmt
JereSalo Apr 24, 2025
2524914
stop popping callframe unnecessarily (#2584)
JereSalo Apr 24, 2025
7a65bed
Merge branch 'main' into levm/cache_rollback
JereSalo Apr 24, 2025
f7b852d
Merge branch 'levm/cache_rollback' of github.com:lambdaclass/ethrex i…
JereSalo Apr 24, 2025
074526b
move function to utils
JereSalo Apr 24, 2025
af22109
cargo fmt
JereSalo Apr 24, 2025
339ce64
move things to utils and change a comment
JereSalo Apr 24, 2025
2d0f635
merge main
JereSalo Apr 25, 2025
efbc932
Merge branch 'main' into levm/cache_rollback
JereSalo Apr 25, 2025
3cfa467
Merge branch 'main' into levm/cache_rollback
JereSalo Apr 25, 2025
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 cmd/ef_tests/state/runner/levm_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ use ethrex_common::{
H256, U256,
};
use ethrex_levm::{
db::gen_db::GeneralizedDatabase,
errors::{ExecutionReport, TxValidationError, VMError},
vm::{EVMConfig, GeneralizedDatabase, VM},
vm::{EVMConfig, VM},
Environment,
};
use ethrex_rlp::encode::RLPEncode;
Expand Down
2 changes: 1 addition & 1 deletion cmd/ef_tests/state/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::{
types::{EFTest, EFTestTransaction},
};
use ethrex_common::{types::Genesis, H256, U256};
use ethrex_levm::{db::CacheDB, vm::GeneralizedDatabase};
use ethrex_levm::db::{gen_db::GeneralizedDatabase, CacheDB};
use ethrex_storage::{EngineType, Store};
use ethrex_vm::{
backends::revm::db::{evm_state, EvmState},
Expand Down
2 changes: 1 addition & 1 deletion crates/l2/utils/prover/save_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ pub fn block_number_has_all_needed_proofs(
#[allow(clippy::expect_used)]
mod tests {
use ethrex_blockchain::Blockchain;
use ethrex_levm::db::gen_db::GeneralizedDatabase;
use ethrex_storage::{EngineType, Store};
use ethrex_vm::{
backends::levm::{CacheDB, LEVM},
Expand All @@ -396,7 +397,6 @@ mod tests {

use super::*;
use crate::utils::test_data_io;
use ethrex_levm::vm::GeneralizedDatabase;
use std::{
fs::{self},
sync::Arc,
Expand Down
3 changes: 2 additions & 1 deletion crates/vm/backends/levm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ use ethrex_common::{
},
Address, H256, U256,
};
use ethrex_levm::db::gen_db::GeneralizedDatabase;
use ethrex_levm::{
errors::{ExecutionReport, TxResult, VMError},
vm::{EVMConfig, GeneralizedDatabase, Substate, VM},
vm::{EVMConfig, Substate, VM},
Account, Environment,
};
use ethrex_storage::error::StoreError;
Expand Down
2 changes: 1 addition & 1 deletion crates/vm/backends/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ use ethrex_common::types::{
AccessList, Block, BlockHeader, Fork, GenericTransaction, Receipt, Transaction, Withdrawal,
};
use ethrex_common::{Address, H256};
use ethrex_levm::db::gen_db::GeneralizedDatabase;
use ethrex_levm::db::CacheDB;
use ethrex_levm::vm::GeneralizedDatabase;
use ethrex_storage::Store;
use ethrex_storage::{error::StoreError, AccountUpdate};
use levm::LEVM;
Expand Down
7 changes: 4 additions & 3 deletions crates/vm/levm/bench/revm_comparison/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ use ethrex_common::{
Address as EthrexAddress, U256,
};
use ethrex_levm::{
db::{cache, CacheDB},
db::{cache, gen_db::GeneralizedDatabase, CacheDB},
errors::{TxResult, VMError},
vm::{GeneralizedDatabase, VM},
vm::VM,
Environment,
};
use ethrex_vm::db::ExecutionDB;
Expand Down Expand Up @@ -84,7 +84,8 @@ pub fn run_with_levm(program: &str, runs: u64, calldata: &str) {
),
);

for _ in 0..runs - 1 {
// when using stateful execute() we have to use nonce when instantiating the vm. Otherwise use 0.
for _nonce in 0..runs - 1 {
let mut vm = new_vm_with_bytecode(&mut db, 0).unwrap();
vm.call_frames.last_mut().unwrap().calldata = calldata.clone();
vm.env.gas_limit = u64::MAX - 1;
Expand Down
7 changes: 6 additions & 1 deletion crates/vm/levm/src/call_frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ use crate::{
memory::Memory,
opcodes::Opcode,
utils::get_valid_jump_destinations,
Account,
};
use bytes::Bytes;
use ethrex_common::{types::Log, Address, U256};
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Stack {
Expand Down Expand Up @@ -85,8 +86,12 @@ pub struct CallFrame {
pub valid_jump_destinations: HashSet<usize>,
/// This is set to true if the function that created this callframe is CREATE or CREATE2
pub create_op_called: bool,
/// Everytime we want to write an account during execution of a callframe we store the pre-write state so that we can restore if it reverts
pub cache_backup: CacheBackup,
}

pub type CacheBackup = HashMap<Address, Option<Account>>;

impl CallFrame {
#[allow(clippy::too_many_arguments)]
pub fn new(
Expand Down
257 changes: 257 additions & 0 deletions crates/vm/levm/src/db/gen_db.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
use std::sync::Arc;

use bytes::Bytes;
use ethrex_common::types::Fork;
use ethrex_common::Address;
use ethrex_common::U256;
use keccak_hash::H256;

use crate::errors::InternalError;
use crate::errors::VMError;
use crate::vm::Substate;
use crate::vm::VM;
use crate::Account;
use crate::AccountInfo;
use crate::StorageSlot;
use std::collections::HashMap;

use super::cache;
use super::error::DatabaseError;
use super::CacheDB;
use super::Database;

#[derive(Clone)]
pub struct GeneralizedDatabase {
pub store: Arc<dyn Database>,
pub cache: CacheDB,
}

impl GeneralizedDatabase {
pub fn new(store: Arc<dyn Database>, cache: CacheDB) -> Self {
Self { store, cache }
}

// ================== Account related functions =====================
/// Gets account, first checking the cache and then the database
/// (caching in the second case)
pub fn get_account(&mut self, address: Address) -> Result<Account, DatabaseError> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we return a reference to the Account here, instead of cloning it? The caller may clone if it needs to.
Cloning too many accounts may be expensive, even more if those Accounts are contracts (contain bytecode and storage).

match cache::get_account(&self.cache, &address) {
Some(acc) => Ok(acc.clone()),
None => {
let account_info = self.store.get_account_info(address)?;
let account = Account {
info: account_info,
storage: HashMap::new(),
};
cache::insert_account(&mut self.cache, address, account.clone());
Ok(account)
}
}
}

/// Gets account without pushing it to the cache
pub fn get_account_no_push_cache(&self, address: Address) -> Result<Account, DatabaseError> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

match cache::get_account(&self.cache, &address) {
Some(acc) => Ok(acc.clone()),
None => {
let account_info = self.store.get_account_info(address)?;
Ok(Account {
info: account_info,
storage: HashMap::new(),
})
}
}
}

/// **Accesses to an account's information.**
///
/// Accessed accounts are stored in the `touched_accounts` set.
/// Accessed accounts take place in some gas cost computation.
pub fn access_account(
&mut self,
accrued_substate: &mut Substate,
address: Address,
) -> Result<(AccountInfo, bool), DatabaseError> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we may be ok by cloning the AccountInfo, since it does not contain the bytecode nor storage

let address_was_cold = accrued_substate.touched_accounts.insert(address);
let account = match cache::get_account(&self.cache, &address) {
Some(account) => account.info.clone(),
None => self.store.get_account_info(address)?,
};
Ok((account, address_was_cold))
}
}

impl<'a> VM<'a> {
// ================== Account related functions =====================

pub fn get_account_mut(&mut self, address: Address) -> Result<&mut Account, VMError> {
if !cache::is_account_cached(&self.db.cache, &address) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Instead of interacting with the db.cache, why don't you make a function for the storage which retrieves what you want?
In this case, we could have a get_account_mut on the GeneralizedDatabase which returns a mutable reference to the Account.
You can then clone that if you want to store it in the backup.

let account_info = self.db.store.get_account_info(address)?;
let account = Account {
info: account_info,
storage: HashMap::new(),
};
cache::insert_account(&mut self.db.cache, address, account.clone());
}

let backup_account = cache::get_account(&self.db.cache, &address)
.ok_or(VMError::Internal(InternalError::AccountNotFound))?
.clone();

if let Ok(frame) = self.current_call_frame_mut() {
frame
.cache_backup
.entry(address)
.or_insert_with(|| Some(backup_account));
}

let account = cache::get_account_mut(&mut self.db.cache, &address)
.ok_or(VMError::Internal(InternalError::AccountNotFound))?;

Ok(account)
}

pub fn increase_account_balance(
&mut self,
address: Address,
increase: U256,
) -> Result<(), VMError> {
let account = self.get_account_mut(address)?;
account.info.balance = account
.info
.balance
.checked_add(increase)
.ok_or(VMError::BalanceOverflow)?;
Ok(())
}

pub fn decrease_account_balance(
&mut self,
address: Address,
decrease: U256,
) -> Result<(), VMError> {
let account = self.get_account_mut(address)?;
account.info.balance = account
.info
.balance
.checked_sub(decrease)
.ok_or(VMError::BalanceUnderflow)?;
Ok(())
}

/// Updates bytecode of given account.
pub fn update_account_bytecode(
&mut self,
address: Address,
new_bytecode: Bytes,
) -> Result<(), VMError> {
let account = self.get_account_mut(address)?;
account.info.bytecode = new_bytecode;
Ok(())
}

// =================== Nonce related functions ======================
pub fn increment_account_nonce(&mut self, address: Address) -> Result<u64, VMError> {
let account = self.get_account_mut(address)?;
account.info.nonce = account
.info
.nonce
.checked_add(1)
.ok_or(VMError::NonceOverflow)?;
Ok(account.info.nonce)
}

/// Inserts account to cache backing up the previus state of it in the CacheBackup (if it wasn't already backed up)
pub fn insert_account(&mut self, address: Address, account: Account) -> Result<(), VMError> {
let previous_account = cache::insert_account(&mut self.db.cache, address, account);

if let Ok(frame) = self.current_call_frame_mut() {
frame
.cache_backup
.entry(address)
.or_insert_with(|| previous_account.as_ref().map(|account| (*account).clone()));
}

Ok(())
}

/// Removes account from cache backing up the previus state of it in the CacheBackup (if it wasn't already backed up)
pub fn remove_account(&mut self, address: Address) -> Result<(), VMError> {
let previous_account = cache::remove_account(&mut self.db.cache, &address);

if let Ok(frame) = self.current_call_frame_mut() {
frame
.cache_backup
.entry(address)
.or_insert_with(|| previous_account.as_ref().map(|account| (*account).clone()));
}

Ok(())
}

/// Accesses to an account's storage slot.
///
/// Accessed storage slots are stored in the `touched_storage_slots` set.
/// Accessed storage slots take place in some gas cost computation.
pub fn access_storage_slot(
&mut self,
address: Address,
key: H256,
) -> Result<(StorageSlot, bool), VMError> {
// [EIP-2929] - Introduced conditional tracking of accessed storage slots for Berlin and later specs.
let mut storage_slot_was_cold = false;
if self.env.config.fork >= Fork::Berlin {
storage_slot_was_cold = self
.accrued_substate
.touched_storage_slots
.entry(address)
.or_default()
.insert(key);
}
let storage_slot = match cache::get_account(&self.db.cache, &address) {
Some(account) => match account.storage.get(&key) {
Some(storage_slot) => storage_slot.clone(),
None => {
let value = self.db.store.get_storage_slot(address, key)?;
StorageSlot {
original_value: value,
current_value: value,
}
}
},
None => {
let value = self.db.store.get_storage_slot(address, key)?;
StorageSlot {
original_value: value,
current_value: value,
}
}
};

// When updating account storage of an account that's not yet cached we need to store the StorageSlot in the account
// Note: We end up caching the account because it is the most straightforward way of doing it.
let account = self.get_account_mut(address)?;
account.storage.insert(key, storage_slot.clone());

Ok((storage_slot, storage_slot_was_cold))
}

pub fn update_account_storage(
&mut self,
address: Address,
key: H256,
new_value: U256,
) -> Result<(), VMError> {
let account = self.get_account_mut(address)?;
let account_original_storage_slot_value = account
.storage
.get(&key)
.map_or(U256::zero(), |slot| slot.original_value);
let slot = account.storage.entry(key).or_insert(StorageSlot {
original_value: account_original_storage_slot_value,
current_value: new_value,
});
slot.current_value = new_value;
Ok(())
}
}
1 change: 1 addition & 0 deletions crates/vm/levm/src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use ethrex_common::{types::ChainConfig, Address, H256, U256};
pub mod cache;
pub use cache::CacheDB;
pub mod error;
pub mod gen_db;

pub trait Database: Send + Sync {
fn get_account_info(&self, address: Address) -> Result<AccountInfo, DatabaseError>;
Expand Down
Loading
Loading