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

VM Storage Cache #209

Merged
merged 5 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ arbitrator/wasm-testsuite/target/
arbitrator/wasm-libraries/target/
arbitrator/tools/wasmer/target/
arbitrator/tools/wasm-tools/
arbitrator/tools/pricers/
arbitrator/tools/module_roots/
arbitrator/langs/rust/target/
arbitrator/langs/bf/target/
Expand Down
1 change: 1 addition & 0 deletions arbitrator/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions arbitrator/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[workspace]
members = [
"arbutil",
"caller-env",
"prover",
"stylus",
"jit",
Expand Down
1 change: 1 addition & 0 deletions arbitrator/arbutil/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ edition = "2021"
[dependencies]
digest = "0.9.0"
eyre = "0.6.5"
fnv = "1.0.7"
hex = "0.4.3"
num-traits = "0.2.17"
siphasher = "0.3.10"
Expand Down
31 changes: 15 additions & 16 deletions arbitrator/arbutil/src/evm/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,24 @@

use crate::{evm::user::UserOutcomeKind, Bytes20, Bytes32};
use eyre::Result;
use num_enum::IntoPrimitive;
use std::sync::Arc;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, IntoPrimitive)]
#[repr(u8)]
pub enum EvmApiStatus {
Success,
Failure,
}

impl From<EvmApiStatus> for UserOutcomeKind {
fn from(value: EvmApiStatus) -> Self {
match value {
EvmApiStatus::Success => UserOutcomeKind::Success,
EvmApiStatus::Failure => UserOutcomeKind::Revert,
}
}
OutOfGas,
WriteProtection,
}

impl From<u8> for EvmApiStatus {
fn from(value: u8) -> Self {
match value {
0 => Self::Success,
2 => Self::OutOfGas,
3 => Self::WriteProtection,
_ => Self::Failure,
}
}
Expand All @@ -34,7 +30,7 @@ impl From<u8> for EvmApiStatus {
#[repr(u32)]
pub enum EvmApiMethod {
GetBytes32,
SetBytes32,
SetTrieSlots,
ContractCall,
DelegateCall,
StaticCall,
Expand Down Expand Up @@ -81,10 +77,13 @@ pub trait EvmApi<D: DataReader>: Send + 'static {
/// Analogous to `vm.SLOAD`.
fn get_bytes32(&mut self, key: Bytes32) -> (Bytes32, u64);

/// Stores the given value at the given key in the EVM state trie.
/// Returns the access cost on success.
/// Analogous to `vm.SSTORE`.
fn set_bytes32(&mut self, key: Bytes32, value: Bytes32) -> Result<u64>;
/// Stores the given value at the given key in Stylus VM's cache of the EVM state trie.
/// Note that the actual values only get written after calls to `set_trie_slots`.
fn cache_bytes32(&mut self, key: Bytes32, value: Bytes32) -> u64;

/// Persists any dirty values in the storage cache to the EVM state trie, dropping the cache entirely if requested.
/// Analogous to repeated invocations of `vm.SSTORE`.
fn flush_storage_cache(&mut self, clear: bool, gas_left: u64) -> Result<u64>;

/// Calls the contract at the given address.
/// Returns the EVM return data's length, the gas cost, and whether the call succeeded.
Expand Down Expand Up @@ -141,7 +140,7 @@ pub trait EvmApi<D: DataReader>: Send + 'static {
) -> (eyre::Result<Bytes20>, u32, u64);

/// Returns the EVM return data.
/// Analogous to `vm.RETURNDATASIZE`.
/// Analogous to `vm.RETURNDATA`.
fn get_return_data(&self) -> D;

/// Emits an EVM log with the given number of topics and data, the first bytes of which should be the topic data.
Expand Down
6 changes: 5 additions & 1 deletion arbitrator/arbutil/src/evm/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// Copyright 2023, Offchain Labs, Inc.
// Copyright 2023-2024, Offchain Labs, Inc.
// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE

use crate::{Bytes20, Bytes32};

pub mod api;
pub mod req;
pub mod storage;
pub mod user;

// params.SstoreSentryGasEIP2200
Expand All @@ -16,6 +17,9 @@ pub const COLD_ACCOUNT_GAS: u64 = 2600;
// params.ColdSloadCostEIP2929
pub const COLD_SLOAD_GAS: u64 = 2100;

// params.WarmStorageReadCostEIP2929
pub const WARM_SLOAD_GAS: u64 = 100;

// params.LogGas and params.LogDataGas
pub const LOG_TOPIC_GAS: u64 = 375;
pub const LOG_DATA_GAS: u64 = 8;
Expand Down
81 changes: 57 additions & 24 deletions arbitrator/arbutil/src/evm/req.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,26 @@

use crate::{
evm::{
api::{DataReader, EvmApi, EvmApiMethod},
api::{DataReader, EvmApi, EvmApiMethod, EvmApiStatus},
storage::{StorageCache, StorageWord},
user::UserOutcomeKind,
},
format::Utf8OrHex,
pricing::EVM_API_INK,
Bytes20, Bytes32,
};
use eyre::{bail, eyre, Result};
use std::collections::hash_map::Entry;

pub trait RequestHandler<D: DataReader>: Send + 'static {
fn handle_request(&mut self, req_type: EvmApiMethod, req_data: &[u8]) -> (Vec<u8>, D, u64);
fn request(&mut self, req_type: EvmApiMethod, req_data: impl AsRef<[u8]>) -> (Vec<u8>, D, u64);
}

pub struct EvmApiRequestor<D: DataReader, H: RequestHandler<D>> {
handler: H,
last_code: Option<(Bytes20, D)>,
last_return_data: Option<D>,
storage_cache: StorageCache,
}

impl<D: DataReader, H: RequestHandler<D>> EvmApiRequestor<D, H> {
Expand All @@ -26,11 +31,12 @@ impl<D: DataReader, H: RequestHandler<D>> EvmApiRequestor<D, H> {
handler,
last_code: None,
last_return_data: None,
storage_cache: StorageCache::default(),
}
}

fn handle_request(&mut self, req_type: EvmApiMethod, req_data: &[u8]) -> (Vec<u8>, D, u64) {
self.handler.handle_request(req_type, req_data)
fn request(&mut self, req_type: EvmApiMethod, req_data: impl AsRef<[u8]>) -> (Vec<u8>, D, u64) {
self.handler.request(req_type, req_data)
}

/// Call out to a contract.
Expand All @@ -48,7 +54,7 @@ impl<D: DataReader, H: RequestHandler<D>> EvmApiRequestor<D, H> {
request.extend(gas.to_be_bytes());
request.extend(input);

let (res, data, cost) = self.handle_request(call_type, &request);
let (res, data, cost) = self.request(call_type, &request);
let status: UserOutcomeKind = res[0].try_into().expect("unknown outcome");
let data_len = data.slice().len() as u32;
self.last_return_data = Some(data);
Expand All @@ -75,7 +81,7 @@ impl<D: DataReader, H: RequestHandler<D>> EvmApiRequestor<D, H> {
}
request.extend(code);

let (mut res, data, cost) = self.handle_request(create_type, &request);
let (mut res, data, cost) = self.request(create_type, request);
if res.len() != 21 || res[0] == 0 {
if !res.is_empty() {
res.remove(0);
Expand All @@ -93,20 +99,47 @@ impl<D: DataReader, H: RequestHandler<D>> EvmApiRequestor<D, H> {

impl<D: DataReader, H: RequestHandler<D>> EvmApi<D> for EvmApiRequestor<D, H> {
fn get_bytes32(&mut self, key: Bytes32) -> (Bytes32, u64) {
let (res, _, cost) = self.handle_request(EvmApiMethod::GetBytes32, key.as_slice());
(res.try_into().unwrap(), cost)
let cache = &mut self.storage_cache;
let mut cost = cache.read_gas();

let value = cache.entry(key).or_insert_with(|| {
let (res, _, gas) = self.handler.request(EvmApiMethod::GetBytes32, key);
cost = cost.saturating_add(gas).saturating_add(EVM_API_INK);
StorageWord::known(res.try_into().unwrap())
});
(value.value, cost)
}

fn set_bytes32(&mut self, key: Bytes32, value: Bytes32) -> Result<u64> {
let mut request = Vec::with_capacity(64);
request.extend(key);
request.extend(value);
let (res, _, cost) = self.handle_request(EvmApiMethod::SetBytes32, &request);
if res.len() != 1 {
bail!("bad response from set_bytes32")
fn cache_bytes32(&mut self, key: Bytes32, value: Bytes32) -> u64 {
let cost = self.storage_cache.write_gas();
match self.storage_cache.entry(key) {
Entry::Occupied(mut key) => key.get_mut().value = value,
Entry::Vacant(slot) => drop(slot.insert(StorageWord::unknown(value))),
};
cost
}

fn flush_storage_cache(&mut self, clear: bool, gas_left: u64) -> Result<u64> {
let mut data = Vec::with_capacity(64 * self.storage_cache.len() + 8);
data.extend(gas_left.to_be_bytes());

for (key, value) in &mut self.storage_cache.slots {
if value.dirty() {
data.extend(*key);
data.extend(*value.value);
value.known = Some(value.value);
}
}
if clear {
self.storage_cache.clear();
}
if res[0] != 1 {
bail!("write protected")
if data.len() == 8 {
return Ok(0); // no need to make request
}

let (res, _, cost) = self.request(EvmApiMethod::SetTrieSlots, data);
if res[0] != EvmApiStatus::Success.into() {
bail!("{}", String::from_utf8_or_hex(res));
}
Ok(cost)
}
Expand Down Expand Up @@ -175,19 +208,20 @@ impl<D: DataReader, H: RequestHandler<D>> EvmApi<D> for EvmApiRequestor<D, H> {
}

fn emit_log(&mut self, data: Vec<u8>, topics: u32) -> Result<()> {
// TODO: remove copy
let mut request = Vec::with_capacity(4 + data.len());
request.extend(topics.to_be_bytes());
request.extend(data);

let (res, _, _) = self.handle_request(EvmApiMethod::EmitLog, &request);
let (res, _, _) = self.request(EvmApiMethod::EmitLog, request);
if !res.is_empty() {
bail!(String::from_utf8(res).unwrap_or("malformed emit-log response".into()))
}
Ok(())
}

fn account_balance(&mut self, address: Bytes20) -> (Bytes32, u64) {
let (res, _, cost) = self.handle_request(EvmApiMethod::AccountBalance, address.as_slice());
let (res, _, cost) = self.request(EvmApiMethod::AccountBalance, address);
(res.try_into().unwrap(), cost)
}

Expand All @@ -201,19 +235,18 @@ impl<D: DataReader, H: RequestHandler<D>> EvmApi<D> for EvmApiRequestor<D, H> {
req.extend(address);
req.extend(gas_left.to_be_bytes());

let (_, data, cost) = self.handle_request(EvmApiMethod::AccountCode, &req);
let (_, data, cost) = self.request(EvmApiMethod::AccountCode, req);
self.last_code = Some((address, data.clone()));
(data, cost)
}

fn account_codehash(&mut self, address: Bytes20) -> (Bytes32, u64) {
let (res, _, cost) = self.handle_request(EvmApiMethod::AccountCodeHash, address.as_slice());
let (res, _, cost) = self.request(EvmApiMethod::AccountCodeHash, address);
(res.try_into().unwrap(), cost)
}

fn add_pages(&mut self, pages: u16) -> u64 {
self.handle_request(EvmApiMethod::AddPages, &pages.to_be_bytes())
.2
self.request(EvmApiMethod::AddPages, pages.to_be_bytes()).2
}

fn capture_hostio(
Expand All @@ -233,6 +266,6 @@ impl<D: DataReader, H: RequestHandler<D>> EvmApi<D> for EvmApiRequestor<D, H> {
request.extend(name.as_bytes());
request.extend(args);
request.extend(outs);
self.handle_request(EvmApiMethod::CaptureHostIO, &request);
self.request(EvmApiMethod::CaptureHostIO, request);
}
}
73 changes: 73 additions & 0 deletions arbitrator/arbutil/src/evm/storage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright 2022-2024, Offchain Labs, Inc.
// For license information, see https://github.com/nitro/blob/master/LICENSE

use crate::Bytes32;
use fnv::FnvHashMap as HashMap;
use std::ops::{Deref, DerefMut};

/// Represents the EVM word at a given key.
#[derive(Debug)]
pub struct StorageWord {
/// The current value of the slot.
pub value: Bytes32,
/// The value in Geth, if known.
pub known: Option<Bytes32>,
}

impl StorageWord {
pub fn known(value: Bytes32) -> Self {
let known = Some(value);
Self { value, known }
}

pub fn unknown(value: Bytes32) -> Self {
Self { value, known: None }
}

pub fn dirty(&self) -> bool {
Some(self.value) != self.known
}
}

#[derive(Default)]
pub struct StorageCache {
pub(crate) slots: HashMap<Bytes32, StorageWord>,
reads: usize,
writes: usize,
}

impl StorageCache {
pub const REQUIRED_ACCESS_GAS: u64 = 10;

pub fn read_gas(&mut self) -> u64 {
self.reads += 1;
match self.reads {
0..=32 => 0,
33..=128 => 2,
_ => 10,
}
}

pub fn write_gas(&mut self) -> u64 {
self.writes += 1;
match self.writes {
0..=8 => 0,
9..=64 => 7,
_ => 10,
}
}
}

impl Deref for StorageCache {
type Target = HashMap<Bytes32, StorageWord>;

fn deref(&self) -> &Self::Target {
&self.slots
}
}

impl DerefMut for StorageCache {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.slots
}
}
13 changes: 13 additions & 0 deletions arbitrator/arbutil/src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,16 @@ impl<T: Debug> DebugBytes for T {
format!("{:?}", self).as_bytes().to_vec()
}
}

pub trait Utf8OrHex {
fn from_utf8_or_hex(data: impl Into<Vec<u8>>) -> String;
}

impl Utf8OrHex for String {
fn from_utf8_or_hex(data: impl Into<Vec<u8>>) -> String {
match String::from_utf8(data.into()) {
Ok(string) => string,
Err(error) => hex::encode(error.as_bytes()),
}
}
}
Loading
Loading