Skip to content

Commit

Permalink
feat(levm): implement Cache and refactor Db (#991)
Browse files Browse the repository at this point in the history
**Motivation**

<!-- Why does this pull request exist? What are its goals? -->
We currently have an in-memory database that should soon be replaced by
the node's in-disk database. For that purpose we want to allow us to
switch between both kinds of databases. The need to implement this PR's
features and refactor the `Db` arose while working on #904.

**Description**

<!-- A clear and concise general description of the changes this PR
introduces -->
This PR includes:
- Adding a `Cache` to store warm accounts. This removes the need of
having `accessed_accounts` and `accessed_storage_slots` sets in
`Substate` because we know that if they are cached then they are warm.
- Making our `Db` implement the `Database` trait and interact with it
only using methods and not it's attributes, so in the future we can
implement that trait for the actual node's database.
- Fix call opcodes and remove delegate attribute from `CallFrame`.

<!-- Link to issues: Resolves #111, Resolves #222 -->

Part of #814.

---------

Co-authored-by: Juani Medone <[email protected]>
Co-authored-by: maximopalopoli <[email protected]>
Co-authored-by: Javier Chatruc <[email protected]>
  • Loading branch information
4 people authored Oct 31, 2024
1 parent acd0365 commit 9c85075
Show file tree
Hide file tree
Showing 13 changed files with 1,239 additions and 998 deletions.
4 changes: 0 additions & 4 deletions crates/vm/levm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,3 @@ hex = "0.4.3"

[features]
ethereum_foundation_tests = []

[profile.test]
opt-level = 3
debug-assertions = true
3 changes: 3 additions & 0 deletions crates/vm/levm/docs/substate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## Substate

`accessed_addresses` and `accessed_storage_keys` follow the structure defined in [EIP 2929](https://eips.ethereum.org/EIPS/eip-2929#specification)
9 changes: 5 additions & 4 deletions crates/vm/levm/src/call_frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,13 @@ pub struct CallFrame {
pub gas_limit: U256,
pub gas_used: U256,
pub pc: usize,
pub msg_sender: Address, // Origin address?
/// Address of the account that sent the message
pub msg_sender: Address,
/// Address of the recipient of the message
pub to: Address,
/// Address of the code to execute. Usually the same as `to`, but can be different
pub code_address: Address,
pub delegate: Option<Address>,
/// Bytecode to execute
pub bytecode: Bytes,
pub msg_value: U256,
pub stack: Stack, // max 1024 in the future
Expand Down Expand Up @@ -98,7 +101,6 @@ impl CallFrame {
msg_sender: Address,
to: Address,
code_address: Address,
delegate: Option<Address>,
bytecode: Bytes,
msg_value: U256,
calldata: Bytes,
Expand All @@ -112,7 +114,6 @@ impl CallFrame {
msg_sender,
to,
code_address,
delegate,
bytecode,
msg_value,
calldata,
Expand Down
1 change: 0 additions & 1 deletion crates/vm/levm/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ pub mod gas_cost {
pub const RETURNDATACOPY_STATIC: U256 = U256([3, 0, 0, 0]);
pub const RETURNDATACOPY_DYNAMIC_BASE: U256 = U256([3, 0, 0, 0]);
pub const ADDRESS: U256 = U256([2, 0, 0, 0]);
pub const BALANCE: U256 = U256([100, 0, 0, 0]);
pub const ORIGIN: U256 = U256([2, 0, 0, 0]);
pub const CALLER: U256 = U256([2, 0, 0, 0]);
pub const BLOCKHASH: U256 = U256([20, 0, 0, 0]);
Expand Down
126 changes: 126 additions & 0 deletions crates/vm/levm/src/db.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
use crate::vm::{Account, AccountInfo, StorageSlot};
use ethereum_types::{Address, U256};
use keccak_hash::H256;
use std::collections::HashMap;

pub trait Database {
fn get_account_info(&self, address: Address) -> AccountInfo;
fn get_storage_slot(&self, address: Address, key: H256) -> U256;
fn get_block_hash(&self, block_number: u64) -> Option<H256>;
}

#[derive(Debug, Default)]
pub struct Db {
pub accounts: HashMap<Address, Account>,
pub block_hashes: HashMap<u64, H256>,
}

// Methods here are for testing purposes only, for initializing the Db with some values
impl Db {
pub fn new() -> Self {
Self {
accounts: HashMap::new(),
block_hashes: HashMap::new(),
}
}

/// Add accounts to database
pub fn add_accounts(&mut self, accounts: Vec<(Address, Account)>) {
self.accounts.extend(accounts);
}

/// Add block hashes to database
pub fn add_block_hashes(&mut self, block_hashes: Vec<(u64, H256)>) {
self.block_hashes.extend(block_hashes);
}

/// Builder method with accounts [for testing only]
pub fn with_accounts(mut self, accounts: HashMap<Address, Account>) -> Self {
self.accounts = accounts;
self
}

/// Builder method with block hashes [for testing only]
pub fn with_block_hashes(mut self, block_hashes: HashMap<u64, H256>) -> Self {
self.block_hashes = block_hashes;
self
}
}

impl Database for Db {
fn get_account_info(&self, address: Address) -> AccountInfo {
self.accounts
.get(&address)
.unwrap_or(&Account::default())
.info
.clone()
}

fn get_storage_slot(&self, address: Address, key: H256) -> U256 {
// both `original_value` and `current_value` should work here because they have the same values on Db
self.accounts
.get(&address)
.unwrap_or(&Account::default())
.storage
.get(&key)
.unwrap_or(&StorageSlot::default())
.original_value
}

fn get_block_hash(&self, block_number: u64) -> Option<H256> {
self.block_hashes.get(&block_number).cloned()
}
}

#[derive(Debug, Default, Clone)]
pub struct Cache {
pub accounts: HashMap<Address, Account>,
}

impl Cache {
pub fn get_account(&self, address: Address) -> Option<&Account> {
self.accounts.get(&address)
}

pub fn get_mut_account(&mut self, address: Address) -> Option<&mut Account> {
self.accounts.get_mut(&address)
}

pub fn get_storage_slot(&self, address: Address, key: H256) -> Option<StorageSlot> {
self.get_account(address)
.expect("Account should have been cached")
.storage
.get(&key)
.cloned()
}

pub fn add_account(&mut self, address: &Address, account: &Account) {
self.accounts.insert(*address, account.clone());
}

pub fn write_account_storage(&mut self, address: &Address, key: H256, slot: StorageSlot) {
self.accounts
.get_mut(address)
.expect("Account should have been cached")
.storage
.insert(key, slot);
}

pub fn increment_account_nonce(&mut self, address: &Address) {
if let Some(account) = self.accounts.get_mut(address) {
account.info.nonce += 1;
}
}

pub fn is_account_cached(&self, address: &Address) -> bool {
self.accounts.contains_key(address)
}

pub fn is_slot_cached(&self, address: &Address, key: H256) -> bool {
self.is_account_cached(address)
&& self
.get_account(*address)
.map(|account| account.storage.contains_key(&key))
.unwrap_or(false)
}
}
1 change: 1 addition & 0 deletions crates/vm/levm/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod block;
pub mod call_frame;
pub mod constants;
pub mod db;
pub mod errors;
pub mod memory;
pub mod opcode_handlers;
Expand Down
16 changes: 12 additions & 4 deletions crates/vm/levm/src/opcode_handlers/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ impl VM {
return Ok(OpcodeSuccess::Continue);
}

if let Some(block_hash) = self.db.block_hashes.get(&block_number) {
let block_number = block_number.as_u64();

if let Some(block_hash) = self.db.get_block_hash(block_number) {
current_call_frame
.stack
.push(U256::from_big_endian(&block_hash.0))?;
.push(U256::from_big_endian(block_hash.as_bytes()))?;
} else {
current_call_frame.stack.push(U256::zero())?;
}
Expand Down Expand Up @@ -125,9 +127,15 @@ impl VM {
) -> Result<OpcodeSuccess, VMError> {
self.increase_consumed_gas(current_call_frame, gas_cost::SELFBALANCE)?;

let balance = self.db.balance(&current_call_frame.code_address);
current_call_frame.stack.push(balance)?;
// the current account should have been cached when the contract was called
let balance = self
.cache
.get_account(current_call_frame.code_address)
.expect("The current account should always be cached")
.info
.balance;

current_call_frame.stack.push(balance)?;
Ok(OpcodeSuccess::Continue)
}

Expand Down
93 changes: 58 additions & 35 deletions crates/vm/levm/src/opcode_handlers/environment.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use super::*;
use crate::{
constants::{call_opcode, WORD_SIZE},
constants::{
call_opcode::{COLD_ADDRESS_ACCESS_COST, WARM_ADDRESS_ACCESS_COST},
WORD_SIZE,
},
vm::word_to_address,
};
use sha3::{Digest, Keccak256};
Expand All @@ -16,11 +19,7 @@ impl VM {
) -> Result<OpcodeSuccess, VMError> {
self.increase_consumed_gas(current_call_frame, gas_cost::ADDRESS)?;

let addr = if current_call_frame.delegate.is_some() {
current_call_frame.msg_sender
} else {
current_call_frame.code_address
};
let addr = current_call_frame.to; // The recipient of the current call.

current_call_frame.stack.push(U256::from(addr.as_bytes()))?;

Expand All @@ -32,13 +31,18 @@ impl VM {
&mut self,
current_call_frame: &mut CallFrame,
) -> Result<OpcodeSuccess, VMError> {
self.increase_consumed_gas(current_call_frame, gas_cost::BALANCE)?;
let address = &word_to_address(current_call_frame.stack.pop()?);

let addr = current_call_frame.stack.pop()?;
if self.cache.is_account_cached(address) {
self.increase_consumed_gas(current_call_frame, WARM_ADDRESS_ACCESS_COST)?;
} else {
self.increase_consumed_gas(current_call_frame, COLD_ADDRESS_ACCESS_COST)?;
self.cache_from_db(address);
};

let balance = self.db.balance(&word_to_address(addr));
current_call_frame.stack.push(balance)?;
let balance = self.cache.get_account(*address).unwrap().info.balance;

current_call_frame.stack.push(balance)?;
Ok(OpcodeSuccess::Continue)
}

Expand Down Expand Up @@ -237,17 +241,23 @@ impl VM {
current_call_frame: &mut CallFrame,
) -> Result<OpcodeSuccess, VMError> {
let address = word_to_address(current_call_frame.stack.pop()?);
let gas_cost = if self.accrued_substate.warm_addresses.contains(&address) {
call_opcode::WARM_ADDRESS_ACCESS_COST

if self.cache.is_account_cached(&address) {
self.increase_consumed_gas(current_call_frame, WARM_ADDRESS_ACCESS_COST)?;
} else {
call_opcode::COLD_ADDRESS_ACCESS_COST
self.increase_consumed_gas(current_call_frame, COLD_ADDRESS_ACCESS_COST)?;
self.cache_from_db(&address);
};

self.increase_consumed_gas(current_call_frame, gas_cost)?;

let code_size = self.db.get_account_bytecode(&address).len();
current_call_frame.stack.push(code_size.into())?;
let bytecode = self
.cache
.get_account(address)
.unwrap()
.info
.bytecode
.clone();

current_call_frame.stack.push(bytecode.len().into())?;
Ok(OpcodeSuccess::Continue)
}

Expand Down Expand Up @@ -277,26 +287,32 @@ impl VM {
let memory_expansion_cost = current_call_frame
.memory
.expansion_cost(dest_offset + size)?;
let address_access_cost = if self.accrued_substate.warm_addresses.contains(&address) {
call_opcode::WARM_ADDRESS_ACCESS_COST
let gas_cost =
gas_cost::EXTCODECOPY_DYNAMIC_BASE * minimum_word_size + memory_expansion_cost;

if self.cache.is_account_cached(&address) {
self.increase_consumed_gas(current_call_frame, gas_cost + WARM_ADDRESS_ACCESS_COST)?;
} else {
call_opcode::COLD_ADDRESS_ACCESS_COST
self.increase_consumed_gas(current_call_frame, gas_cost + COLD_ADDRESS_ACCESS_COST)?;
self.cache_from_db(&address);
};
let gas_cost = gas_cost::EXTCODECOPY_DYNAMIC_BASE * minimum_word_size
+ memory_expansion_cost
+ address_access_cost;

self.increase_consumed_gas(current_call_frame, gas_cost)?;
let mut bytecode = self
.cache
.get_account(address)
.unwrap()
.info
.bytecode
.clone();

let mut code = self.db.get_account_bytecode(&address);
if code.len() < offset + size {
let mut extended_code = code.to_vec();
if bytecode.len() < offset + size {
let mut extended_code = bytecode.to_vec();
extended_code.resize(offset + size, 0);
code = Bytes::from(extended_code);
bytecode = Bytes::from(extended_code);
}
current_call_frame
.memory
.store_bytes(dest_offset, &code[offset..offset + size]);
.store_bytes(dest_offset, &bytecode[offset..offset + size]);

Ok(OpcodeSuccess::Continue)
}
Expand Down Expand Up @@ -364,17 +380,24 @@ impl VM {
current_call_frame: &mut CallFrame,
) -> Result<OpcodeSuccess, VMError> {
let address = word_to_address(current_call_frame.stack.pop()?);
let gas_cost = if self.accrued_substate.warm_addresses.contains(&address) {
call_opcode::WARM_ADDRESS_ACCESS_COST

if self.cache.is_account_cached(&address) {
self.increase_consumed_gas(current_call_frame, WARM_ADDRESS_ACCESS_COST)?;
} else {
call_opcode::COLD_ADDRESS_ACCESS_COST
self.increase_consumed_gas(current_call_frame, COLD_ADDRESS_ACCESS_COST)?;
self.cache_from_db(&address);
};

self.increase_consumed_gas(current_call_frame, gas_cost)?;
let bytecode = self
.cache
.get_account(address)
.unwrap()
.info
.bytecode
.clone();

let code = self.db.get_account_bytecode(&address);
let mut hasher = Keccak256::new();
hasher.update(code);
hasher.update(bytecode);
let result = hasher.finalize();
current_call_frame
.stack
Expand Down
Loading

0 comments on commit 9c85075

Please sign in to comment.