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

Transient Storage Host I/Os #224

Merged
merged 3 commits into from
Apr 12, 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
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
Loading