Skip to content

Commit

Permalink
Merge pull request #224 from OffchainLabs/trans-ops
Browse files Browse the repository at this point in the history
Transient Storage Host I/Os
  • Loading branch information
rachel-bousfield authored Apr 12, 2024
2 parents 0e2e4da + c07f375 commit 35af7ac
Show file tree
Hide file tree
Showing 17 changed files with 279 additions and 41 deletions.
10 changes: 10 additions & 0 deletions arbitrator/arbutil/src/evm/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ impl From<u8> for EvmApiStatus {
pub enum EvmApiMethod {
GetBytes32,
SetTrieSlots,
GetTransientBytes32,
SetTransientBytes32,
ContractCall,
DelegateCall,
StaticCall,
Expand Down Expand Up @@ -85,6 +87,14 @@ pub trait EvmApi<D: DataReader>: Send + 'static {
/// Analogous to repeated invocations of `vm.SSTORE`.
fn flush_storage_cache(&mut self, clear: bool, gas_left: u64) -> Result<u64>;

/// Reads the 32-byte value in the EVM's transient state trie at offset `key`.
/// Analogous to `vm.TLOAD`.
fn get_transient_bytes32(&mut self, key: Bytes32) -> Bytes32;

/// Writes the 32-byte value in the EVM's transient state trie at offset `key`.
/// Analogous to `vm.TSTORE`.
fn set_transient_bytes32(&mut self, key: Bytes32, value: Bytes32) -> Result<()>;

/// Calls the contract at the given address.
/// Returns the EVM return data's length, the gas cost, and whether the call succeeded.
/// Analogous to `vm.CALL`.
Expand Down
4 changes: 4 additions & 0 deletions arbitrator/arbutil/src/evm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ pub const COLD_SLOAD_GAS: u64 = 2100;
// params.WarmStorageReadCostEIP2929
pub const WARM_SLOAD_GAS: u64 = 100;

// params.WarmStorageReadCostEIP2929 (see enable1153 in jump_table.go)
pub const TLOAD_GAS: u64 = WARM_SLOAD_GAS;
pub const TSTORE_GAS: u64 = WARM_SLOAD_GAS;

// params.LogGas and params.LogDataGas
pub const LOG_TOPIC_GAS: u64 = 375;
pub const LOG_DATA_GAS: u64 = 8;
Expand Down
16 changes: 16 additions & 0 deletions arbitrator/arbutil/src/evm/req.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,22 @@ impl<D: DataReader, H: RequestHandler<D>> EvmApi<D> for EvmApiRequestor<D, H> {
Ok(cost)
}

fn get_transient_bytes32(&mut self, key: Bytes32) -> Bytes32 {
let (res, ..) = self.request(EvmApiMethod::GetTransientBytes32, key);
res.try_into().unwrap()
}

fn set_transient_bytes32(&mut self, key: Bytes32, value: Bytes32) -> Result<()> {
let mut data = Vec::with_capacity(64);
data.extend(key);
data.extend(value);
let (res, ..) = self.request(EvmApiMethod::SetTransientBytes32, data);
if res[0] != EvmApiStatus::Success.into() {
bail!("{}", String::from_utf8_or_hex(res));
}
Ok(())
}

fn contract_call(
&mut self,
contract: Bytes20,
Expand Down
16 changes: 16 additions & 0 deletions arbitrator/stylus/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,22 @@ pub(crate) fn storage_flush_cache<D: DataReader, E: EvmApi<D>>(
hostio!(env, storage_flush_cache(clear != 0))
}

pub(crate) fn transient_load_bytes32<D: DataReader, E: EvmApi<D>>(
mut env: WasmEnvMut<D, E>,
key: GuestPtr,
dest: GuestPtr,
) -> MaybeEscape {
hostio!(env, transient_load_bytes32(key, dest))
}

pub(crate) fn transient_store_bytes32<D: DataReader, E: EvmApi<D>>(
mut env: WasmEnvMut<D, E>,
key: GuestPtr,
value: GuestPtr,
) -> MaybeEscape {
hostio!(env, transient_store_bytes32(key, value))
}

pub(crate) fn call_contract<D: DataReader, E: EvmApi<D>>(
mut env: WasmEnvMut<D, E>,
contract: GuestPtr,
Expand Down
4 changes: 4 additions & 0 deletions arbitrator/stylus/src/native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ impl<D: DataReader, E: EvmApi<D>> NativeInstance<D, E> {
"storage_load_bytes32" => func!(host::storage_load_bytes32),
"storage_cache_bytes32" => func!(host::storage_cache_bytes32),
"storage_flush_cache" => func!(host::storage_flush_cache),
"transient_load_bytes32" => func!(host::transient_load_bytes32),
"transient_store_bytes32" => func!(host::transient_store_bytes32),
"call_contract" => func!(host::call_contract),
"delegate_call_contract" => func!(host::delegate_call_contract),
"static_call_contract" => func!(host::static_call_contract),
Expand Down Expand Up @@ -342,6 +344,8 @@ pub fn module(wasm: &[u8], compile: CompileConfig) -> Result<Vec<u8>> {
"storage_load_bytes32" => stub!(|_: u32, _: u32|),
"storage_cache_bytes32" => stub!(|_: u32, _: u32|),
"storage_flush_cache" => stub!(|_: u32|),
"transient_load_bytes32" => stub!(|_: u32, _: u32|),
"transient_store_bytes32" => stub!(|_: u32, _: u32|),
"call_contract" => stub!(u8 <- |_: u32, _: u32, _: u32, _: u32, _: u64, _: u32|),
"delegate_call_contract" => stub!(u8 <- |_: u32, _: u32, _: u32, _: u64, _: u32|),
"static_call_contract" => stub!(u8 <- |_: u32, _: u32, _: u32, _: u64, _: u32|),
Expand Down
8 changes: 8 additions & 0 deletions arbitrator/stylus/src/test/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ impl EvmApi<VecReader> for TestEvmApi {
Ok(22100 * storage.len() as u64) // pretend worst case
}

fn get_transient_bytes32(&mut self, _key: Bytes32) -> Bytes32 {
unimplemented!("tload not supported")
}

fn set_transient_bytes32(&mut self, _key: Bytes32, _value: Bytes32) -> Result<()> {
unimplemented!("tstore not supported")
}

/// Simulates a contract call.
/// Note: this call function is for testing purposes only and deviates from onchain behavior.
fn contract_call(
Expand Down
42 changes: 30 additions & 12 deletions arbitrator/stylus/tests/storage/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,39 @@ use stylus_sdk::{
stylus_proc::entrypoint,
};

#[link(wasm_import_module = "vm_hooks")]
extern "C" {
fn transient_load_bytes32(key: *const u8, dest: *mut u8);
fn transient_store_bytes32(key: *const u8, value: *const u8);
}

#[entrypoint]
fn user_main(input: Vec<u8>) -> Result<Vec<u8>, Vec<u8>> {
let read = input[0] == 0;
let slot = B256::try_from(&input[1..33]).unwrap();

Ok(if read {
console!("read {slot}");
let data = StorageCache::get_word(slot.into());
console!("value {data}");
data.0.into()
} else {
console!("write {slot}");
let data = B256::try_from(&input[33..]).unwrap();
unsafe { StorageCache::set_word(slot.into(), data) };
console!(("value {data}"));
vec![]
Ok(match input[0] {
0 => {
console!("read {slot}");
let data = StorageCache::get_word(slot.into());
console!("value {data}");
data.0.into()
}
1 => {
console!("write {slot}");
let data = B256::try_from(&input[33..]).unwrap();
unsafe { StorageCache::set_word(slot.into(), data) };
console!(("value {data}"));
vec![]
}
2 => unsafe {
let mut data = [0; 32];
transient_load_bytes32(slot.as_ptr(), data.as_mut_ptr());
data.into()
}
_ => unsafe {
let data = B256::try_from(&input[33..]).unwrap();
transient_store_bytes32(slot.as_ptr(), data.as_ptr());
vec![]
}
})
}
4 changes: 3 additions & 1 deletion arbitrator/wasm-libraries/forward/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ use std::{fs::File, io::Write, path::PathBuf};
use structopt::StructOpt;

/// order matters!
const HOSTIOS: [[&str; 3]; 35] = [
const HOSTIOS: [[&str; 3]; 37] = [
["read_args", "i32", ""],
["write_result", "i32 i32", ""],
["exit_early", "i32", ""],
["storage_load_bytes32", "i32 i32", ""],
["storage_cache_bytes32", "i32 i32", ""],
["storage_flush_cache", "i32", ""],
["transient_load_bytes32", "i32 i32", ""],
["transient_store_bytes32", "i32 i32", ""],
["call_contract", "i32 i32 i32 i32 i64 i32", "i32"],
["delegate_call_contract", "i32 i32 i32 i64 i32", "i32"],
["static_call_contract", "i32 i32 i32 i64 i32", "i32"],
Expand Down
4 changes: 2 additions & 2 deletions arbitrator/wasm-libraries/host-io/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@

#![allow(clippy::missing_safety_doc)] // TODO: add safety docs

use arbutil::PreimageType;
use caller_env::{static_caller::STATIC_MEM, GuestPtr, MemAccess};
use core::ops::{Deref, DerefMut, Index, RangeTo};
use core::convert::TryInto;
use arbutil::PreimageType;
use core::ops::{Deref, DerefMut, Index, RangeTo};

extern "C" {
pub fn wavm_get_globalstate_bytes32(idx: u32, ptr: *mut u8);
Expand Down
32 changes: 32 additions & 0 deletions arbitrator/wasm-libraries/user-host-trait/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,38 @@ pub trait UserHost<DR: DataReader>: GasMeteredMachine {
trace!("storage_flush_cache", self, [be!(clear as u8)], &[])
}

/// Reads a 32-byte value from transient storage. Stylus's storage format is identical to
/// that of the EVM. This means that, under the hood, this hostio is accessing the 32-byte
/// value stored in the EVM's transient state trie at offset `key`, which will be `0` when not previously
/// set. The semantics, then, are equivalent to that of the EVM's [`TLOAD`] opcode.
///
/// [`TLOAD`]: https://www.evm.codes/#5c
fn transient_load_bytes32(&mut self, key: GuestPtr, dest: GuestPtr) -> Result<(), Self::Err> {
self.buy_ink(HOSTIO_INK + 2 * PTR_INK + EVM_API_INK)?;
self.buy_gas(evm::TLOAD_GAS)?;

let key = self.read_bytes32(key)?;
let value = self.evm_api().get_transient_bytes32(key);
self.write_bytes32(dest, value)?;
trace!("transient_load_bytes32", self, key, value)
}

/// Writes a 32-byte value to transient storage. Stylus's storage format is identical to that
/// of the EVM. This means that, under the hood, this hostio represents storing a 32-byte value into
/// the EVM's transient state trie at offset `key`. The semantics, then, are equivalent to that of the
/// EVM's [`TSTORE`] opcode.
///
/// [`TSTORE`]: https://www.evm.codes/#5d
fn transient_store_bytes32(&mut self, key: GuestPtr, value: GuestPtr) -> Result<(), Self::Err> {
self.buy_ink(HOSTIO_INK + 2 * PTR_INK + EVM_API_INK)?;
self.buy_gas(evm::TSTORE_GAS)?;

let key = self.read_bytes32(key)?;
let value = self.read_bytes32(value)?;
self.evm_api().set_transient_bytes32(key, value)?;
trace!("transient_store_bytes32", self, [key, value], &[])
}

/// Calls the contract at the given address with options for passing value and to limit the
/// amount of gas supplied. The return status indicates whether the call succeeded, and is
/// nonzero on failure.
Expand Down
10 changes: 10 additions & 0 deletions arbitrator/wasm-libraries/user-host/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ pub unsafe extern "C" fn user_host__storage_flush_cache(clear: u32) {
hostio!(storage_flush_cache(clear != 0))
}

#[no_mangle]
pub unsafe extern "C" fn user_host__transient_load_bytes32(key: GuestPtr, dest: GuestPtr) {
hostio!(transient_load_bytes32(key, dest))
}

#[no_mangle]
pub unsafe extern "C" fn user_host__transient_store_bytes32(key: GuestPtr, value: GuestPtr) {
hostio!(transient_store_bytes32(key, value))
}

#[no_mangle]
pub unsafe extern "C" fn user_host__call_contract(
contract: GuestPtr,
Expand Down
10 changes: 10 additions & 0 deletions arbitrator/wasm-libraries/user-test/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ pub unsafe extern "C" fn vm_hooks__storage_flush_cache(clear: u32) {
hostio!(storage_flush_cache(clear != 0))
}

#[no_mangle]
pub unsafe extern "C" fn vm_hooks__transient_load_bytes32(key: GuestPtr, dest: GuestPtr) {
hostio!(transient_load_bytes32(key, dest))
}

#[no_mangle]
pub unsafe extern "C" fn vm_hooks__transient_store_bytes32(key: GuestPtr, value: GuestPtr) {
hostio!(transient_store_bytes32(key, value))
}

#[no_mangle]
pub unsafe extern "C" fn vm_hooks__call_contract(
contract: GuestPtr,
Expand Down
8 changes: 8 additions & 0 deletions arbitrator/wasm-libraries/user-test/src/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,14 @@ impl EvmApi<VecReader> for MockEvmApi {
Ok(22100 * KEYS.lock().len() as u64) // pretend worst case
}

fn get_transient_bytes32(&mut self, _key: Bytes32) -> Bytes32 {
unimplemented!()
}

fn set_transient_bytes32(&mut self, _key: Bytes32, _value: Bytes32) -> Result<()> {
unimplemented!()
}

/// Simulates a contract call.
/// Note: this call function is for testing purposes only and deviates from onchain behavior.
fn contract_call(
Expand Down
21 changes: 21 additions & 0 deletions arbos/programs/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ type RequestType int
const (
GetBytes32 RequestType = iota
SetTrieSlots
GetTransientBytes32
SetTransientBytes32
ContractCall
DelegateCall
StaticCall
Expand Down Expand Up @@ -97,6 +99,16 @@ func newApiClosures(
}
return Success
}
getTransientBytes32 := func(key common.Hash) common.Hash {
return db.GetTransientState(actingAddress, key)
}
setTransientBytes32 := func(key, value common.Hash) apiStatus {
if readOnly {
return WriteProtection
}
db.SetTransientState(actingAddress, key, value)
return Success
}
doCall := func(
contract common.Address, opcode vm.OpCode, input []byte, gas uint64, value *big.Int,
) ([]byte, uint64, error) {
Expand Down Expand Up @@ -317,6 +329,15 @@ func newApiClosures(
gas := gasLeft
status := setTrieSlots(takeRest(), &gas)
return status.to_slice(), nil, gasLeft - gas
case GetTransientBytes32:
key := takeHash()
out := getTransientBytes32(key)
return out[:], nil, 0
case SetTransientBytes32:
key := takeHash()
value := takeHash()
status := setTransientBytes32(key, value)
return status.to_slice(), nil, 0
case ContractCall, DelegateCall, StaticCall:
var opcode vm.OpCode
switch req {
Expand Down
Loading

0 comments on commit 35af7ac

Please sign in to comment.