From a3f203e5abe4c2417e1a3bed7018af63f4509bd8 Mon Sep 17 00:00:00 2001 From: Gabriel de Quadros Ligneul <8294320+gligneul@users.noreply.github.com> Date: Tue, 13 Aug 2024 17:39:57 -0300 Subject: [PATCH] Update Stylus Replay with changes on the HostIO API (#73) We introduced some changes to the HostIO API on the nitro repository. This commit updates the Stylus Replay tool to reflect these changes. Co-authored-by: Raul Jordan --- replay/src/hostio.rs | 227 ++++++++++++++++++++++++++++++++++++------- replay/src/trace.rs | 133 ++++++++++++++++++++----- 2 files changed, 299 insertions(+), 61 deletions(-) diff --git a/replay/src/hostio.rs b/replay/src/hostio.rs index ce03b56..d47b218 100644 --- a/replay/src/hostio.rs +++ b/replay/src/hostio.rs @@ -53,11 +53,26 @@ pub unsafe extern "C" fn read_args(dest: *mut u8) { /// naturally when `user_entrypoint` returns. #[named] #[no_mangle] -pub unsafe extern "C" fn write_result(data: *const u8, len: usize) { +pub unsafe extern "C" fn write_result(data: *const u8, len: u32) { frame!(WriteResult { result }); assert_eq!(read_bytes(data, len), &*result); } +/// Exits program execution early with the given status code. +/// If `0`, the program returns successfully with any data supplied by `write_result`. +/// Otherwise, the program reverts and treats any `write_result` data as revert data. +/// +/// The semantics are equivalent to that of the EVM's [`Return`] and [`Revert`] opcodes. +/// Note: this function just traces, it's up to the caller to actually perform the exit. +/// +/// [`Return`]: https://www.evm.codes/#f3 +/// [`Revert`]: https://www.evm.codes/#fd +#[named] +#[no_mangle] +pub unsafe extern "C" fn exit_early(status: u32) { + frame!(ExitEarly { status }); +} + /// Reads a 32-byte value from permanent 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 state trie at offset `key`, which will be `0` when not previously @@ -72,32 +87,61 @@ pub unsafe extern "C" fn storage_load_bytes32(key_ptr: *const u8, dest: *mut u8) copy!(value, dest); } -/// Stores a 32-byte value to permanent storage. Stylus's storage format is identical to that -/// of the EVM. This means that, under the hood, this hostio is storing a 32-byte value into -/// the EVM state trie at offset `key`. Furthermore, refunds are tabulated exactly as in the -/// EVM. The semantics, then, are equivalent to that of the EVM's [`SSTORE`] opcode. +/// Writes a 32-byte value to the permanent storage cache. 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 state trie at offset `key`. Refunds are tabulated exactly as in the EVM. The semantics, then, +/// are equivalent to that of the EVM's [`SSTORE`] opcode. +/// +/// Note: because this value is cached, one must call `storage_flush_cache` to persist the value. +/// +/// Auditor's note: we require the [`SSTORE`] sentry per EVM rules. The `gas_cost` returned by the EVM API +/// may exceed this amount, but that's ok because the predominant cost is due to state bloat concerns. /// /// [`SSTORE`]: https://www.evm.codes/#55 #[named] #[no_mangle] -pub unsafe extern "C" fn storage_store_bytes32(key_ptr: *const u8, value_ptr: *const u8) { - frame!(StorageStoreBytes32 { key, value }); +pub unsafe extern "C" fn storage_cache_bytes32(key_ptr: *const u8, value_ptr: *const u8) { + frame!(StorageCacheBytes32 { key, value }); assert_eq!(read_fixed(key_ptr), key); assert_eq!(read_fixed(value_ptr), value); } +/// Persists any dirty values in the storage cache to the EVM state trie, dropping the cache entirely if requested. +/// Analogous to repeated invocations of [`SSTORE`]. +/// +/// [`SSTORE`]: https://www.evm.codes/#55 #[named] #[no_mangle] -pub unsafe extern "C" fn storage_cache_bytes32(key_ptr: *const u8, value_ptr: *const u8) { - frame!(StorageCacheBytes32 { key, value }); +pub unsafe extern "C" fn storage_flush_cache(clear: u32) { + frame!(StorageFlushCache { clear }); +} + +/// 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 +#[named] +#[no_mangle] +pub unsafe extern "C" fn transient_load_bytes32(key_ptr: *const u8, dest: *mut u8) { + frame!(TransientLoadBytes32 { key, value }); assert_eq!(read_fixed(key_ptr), key); - assert_eq!(read_fixed(value_ptr), value); + copy!(value, dest); } +/// 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 #[named] #[no_mangle] -pub unsafe extern "C" fn storage_flush_cache(clear: u32) { - frame!(StorageFlushCache { clear }); +pub unsafe extern "C" fn transient_store_bytes32(key_ptr: *const u8, value_ptr: *const u8) { + frame!(TransientStoreBytes32 { key, value }); + assert_eq!(read_fixed(key_ptr), key); + assert_eq!(read_fixed(value_ptr), value); } /// Gets the ETH balance in wei of the account at the given address. @@ -112,6 +156,45 @@ pub unsafe extern "C" fn account_balance(address_ptr: *const u8, dest: *mut u8) copy!(balance.to_be_bytes::<32>(), dest); } +/// Gets a subset of the code from the account at the given address. The semantics are identical to that +/// of the EVM's [`EXT_CODE_COPY`] opcode, aside from one small detail: the write to the buffer `dest` will +/// stop after the last byte is written. This is unlike the EVM, which right pads with zeros in this scenario. +/// The return value is the number of bytes written, which allows the caller to detect if this has occured. +/// +/// [`EXT_CODE_COPY`]: https://www.evm.codes/#3C +#[named] +#[no_mangle] +pub unsafe extern "C" fn account_code( + address_ptr: *const u8, + offset_recv: u32, + size_recv: u32, + dest: *mut u8, +) -> u32 { + frame!(AccountCode { + address, + offset, + size, + code + }); + assert_eq!(offset_recv, offset); + assert_eq!(size_recv, size); + assert_eq!(read_fixed(address_ptr), address); + copy!(code, dest); + code.len() as u32 +} + +/// Gets the size of the code in bytes at the given address. The semantics are equivalent +/// to that of the EVM's [`EXT_CODESIZE`]. +/// +/// [`EXT_CODESIZE`]: https://www.evm.codes/#3B +#[named] +#[no_mangle] +pub unsafe extern "C" fn account_code_size(address_ptr: *const u8) -> u32 { + frame!(AccountCodeSize { address, size }); + assert_eq!(read_fixed(address_ptr), address); + size +} + /// Gets the code hash of the account at the given address. The semantics are equivalent /// to that of the EVM's [`EXT_CODEHASH`] opcode. Note that the code hash of an account without /// code will be the empty hash @@ -215,10 +298,10 @@ pub unsafe extern "C" fn chainid() -> u64 { pub unsafe extern "C" fn call_contract( address_ptr: *const u8, calldata: *const u8, - calldata_len: usize, + calldata_len: u32, value_ptr: *const u8, gas_supplied: u64, - return_data_len: *mut usize, + return_data_len: *mut u32, ) -> u8 { frame!(CallContract { address, @@ -233,7 +316,7 @@ pub unsafe extern "C" fn call_contract( assert_eq!(read_bytes(calldata, calldata_len), &*data); assert_eq!(read_fixed(value_ptr), value.to_be_bytes::<32>()); assert_eq!(gas_supplied, gas); - *return_data_len = outs_len as usize; + *return_data_len = outs_len; status } @@ -256,9 +339,9 @@ pub unsafe extern "C" fn call_contract( pub unsafe extern "C" fn delegate_call_contract( address_ptr: *const u8, calldata: *const u8, - calldata_len: usize, + calldata_len: u32, gas_supplied: u64, - return_data_len: *mut usize, + return_data_len: *mut u32, ) -> u8 { frame!(DelegateCallContract { address, @@ -271,7 +354,7 @@ pub unsafe extern "C" fn delegate_call_contract( assert_eq!(read_fixed(address_ptr), address); assert_eq!(read_bytes(calldata, calldata_len), &*data); assert_eq!(gas_supplied, gas); - *return_data_len = outs_len as usize; + *return_data_len = outs_len; status } @@ -294,9 +377,9 @@ pub unsafe extern "C" fn delegate_call_contract( pub unsafe extern "C" fn static_call_contract( address_ptr: *const u8, calldata: *const u8, - calldata_len: usize, + calldata_len: u32, gas_supplied: u64, - return_data_len: *mut usize, + return_data_len: *mut u32, ) -> u8 { frame!(StaticCallContract { address, @@ -309,7 +392,7 @@ pub unsafe extern "C" fn static_call_contract( assert_eq!(read_fixed(address_ptr), address); assert_eq!(read_bytes(calldata, calldata_len), &*data); assert_eq!(gas_supplied, gas); - *return_data_len = outs_len as usize; + *return_data_len = outs_len; status } @@ -343,10 +426,10 @@ pub unsafe extern "C" fn contract_address(dest: *mut u8) { #[no_mangle] pub unsafe extern "C" fn create1( code_ptr: *const u8, - code_len: usize, + code_len: u32, value: *const u8, contract: *mut u8, - revert_data_len_ptr: *mut usize, + revert_data_len_ptr: *mut u32, ) { frame!(Create1 { code, @@ -379,11 +462,11 @@ pub unsafe extern "C" fn create1( #[no_mangle] pub unsafe extern "C" fn create2( code_ptr: *const u8, - code_len: usize, + code_len: u32, value_ptr: *const u8, salt_ptr: *const u8, contract: *mut u8, - revert_data_len_ptr: *mut usize, + revert_data_len_ptr: *mut u32, ) { frame!(Create2 { code, @@ -411,7 +494,7 @@ pub unsafe extern "C" fn create2( /// [`LOG4`]: https://www.evm.codes/#a4 #[named] #[no_mangle] -pub unsafe extern "C" fn emit_log(data_ptr: *const u8, len: usize, topic_count: usize) { +pub unsafe extern "C" fn emit_log(data_ptr: *const u8, len: u32, topic_count: u32) { frame!(EmitLog { data, topics }); assert_eq!(read_bytes(data_ptr, len), &*data); assert_eq!(topics, topic_count); @@ -452,6 +535,77 @@ pub unsafe extern "C" fn pay_for_memory_grow(new_pages: u16) { assert_eq!(new_pages, pages); } +/// Computes `value รท exponent` using 256-bit math, writing the result to the first. +/// The semantics are equivalent to that of the EVM's [`DIV`] opcode, which means that a `divisor` of `0` +/// writes `0` to `value`. +/// +/// [`DIV`]: https://www.evm.codes/#04 +#[named] +#[no_mangle] +pub unsafe fn math_div(value: *mut u8, divisor: *const u8) { + frame!(MathDiv { a, b, result }); + assert_eq!(read_fixed(value), a.to_be_bytes::<32>()); + assert_eq!(read_fixed(divisor), b.to_be_bytes::<32>()); + copy!(result.to_be_bytes::<32>(), value); +} + +/// Computes `value % exponent` using 256-bit math, writing the result to the first. +/// The semantics are equivalent to that of the EVM's [`MOD`] opcode, which means that a `modulus` of `0` +/// writes `0` to `value`. +/// +/// [`MOD`]: https://www.evm.codes/#06 +#[named] +#[no_mangle] +pub unsafe fn math_mod(value: *mut u8, modulus: *const u8) { + frame!(MathMod { a, b, result }); + assert_eq!(read_fixed(value), a.to_be_bytes::<32>()); + assert_eq!(read_fixed(modulus), b.to_be_bytes::<32>()); + copy!(result.to_be_bytes::<32>(), value); +} + +/// Computes `value ^ exponent` using 256-bit math, writing the result to the first. +/// The semantics are equivalent to that of the EVM's [`EXP`] opcode. +/// +/// [`EXP`]: https://www.evm.codes/#0A +#[named] +#[no_mangle] +pub unsafe fn math_pow(value: *mut u8, exponent: *const u8) { + frame!(MathPow { a, b, result }); + assert_eq!(read_fixed(value), a.to_be_bytes::<32>()); + assert_eq!(read_fixed(exponent), b.to_be_bytes::<32>()); + copy!(result.to_be_bytes::<32>(), value); +} + +/// Computes `(value + addend) % modulus` using 256-bit math, writing the result to the first. +/// The semantics are equivalent to that of the EVM's [`ADDMOD`] opcode, which means that a `modulus` of `0` +/// writes `0` to `value`. +/// +/// [`ADDMOD`]: https://www.evm.codes/#08 +#[named] +#[no_mangle] +pub unsafe fn math_add_mod(value: *mut u8, addend: *const u8, modulus: *const u8) { + frame!(MathAddMod { a, b, c, result }); + assert_eq!(read_fixed(value), a.to_be_bytes::<32>()); + assert_eq!(read_fixed(addend), b.to_be_bytes::<32>()); + assert_eq!(read_fixed(modulus), c.to_be_bytes::<32>()); + copy!(result.to_be_bytes::<32>(), value); +} + +/// Computes `(value * multiplier) % modulus` using 256-bit math, writing the result to the first. +/// The semantics are equivalent to that of the EVM's [`MULMOD`] opcode, which means that a `modulus` of `0` +/// writes `0` to `value`. +/// +/// [`MULMOD`]: https://www.evm.codes/#09 +#[named] +#[no_mangle] +pub unsafe fn math_mul_mod(value: *mut u8, multiplier: *const u8, modulus: *const u8) { + frame!(MathAddMod { a, b, c, result }); + assert_eq!(read_fixed(value), a.to_be_bytes::<32>()); + assert_eq!(read_fixed(multiplier), b.to_be_bytes::<32>()); + assert_eq!(read_fixed(modulus), c.to_be_bytes::<32>()); + copy!(result.to_be_bytes::<32>(), value); +} + /// Whether the current call is reentrant. #[named] #[no_mangle] @@ -495,7 +649,7 @@ pub unsafe extern "C" fn msg_value(dest: *mut u8) { /// [`SHA3`]: https://www.evm.codes/#20 #[named] #[no_mangle] -pub unsafe extern "C" fn native_keccak256(bytes: *const u8, len: usize, output: *mut u8) { +pub unsafe extern "C" fn native_keccak256(bytes: *const u8, len: u32, output: *mut u8) { frame!(NativeKeccak256 { preimage, digest }); assert_eq!(read_bytes(bytes, len), &*preimage); copy!(digest, output); @@ -510,14 +664,14 @@ pub unsafe extern "C" fn native_keccak256(bytes: *const u8, len: usize, output: #[no_mangle] pub unsafe extern "C" fn read_return_data( dest: *mut u8, - offset_value: usize, - size_value: usize, -) -> usize { + offset_value: u32, + size_value: u32, +) -> u32 { frame!(ReadReturnData { offset, size, data }); - assert_eq!(offset_value, offset as usize); - assert_eq!(size_value, size as usize); + assert_eq!(offset_value, offset); + assert_eq!(size_value, size); copy!(data, dest, data.len()); - data.len() + data.len() as u32 } /// Returns the length of the last EVM call or deployment return result, or `0` if neither have @@ -527,7 +681,7 @@ pub unsafe extern "C" fn read_return_data( /// [`RETURN_DATA_SIZE`]: https://www.evm.codes/#3d #[named] #[no_mangle] -pub unsafe extern "C" fn return_data_size() -> usize { +pub unsafe extern "C" fn return_data_size() -> u32 { frame!(ReturnDataSize { size }); size } @@ -604,7 +758,7 @@ pub unsafe extern "C" fn log_i64(value: i64) { /// Prints a UTF-8 encoded string to the console. Only available in debug mode. #[named] #[no_mangle] -pub unsafe extern "C" fn log_txt(text_ptr: *const u8, len: usize) { +pub unsafe extern "C" fn log_txt(text_ptr: *const u8, len: u32) { frame!(ConsoleLogText { text }); assert_eq!(read_bytes(text_ptr, len), &*text); } @@ -615,7 +769,8 @@ unsafe fn read_fixed(ptr: *const u8) -> [u8; N] { value.assume_init() } -unsafe fn read_bytes(ptr: *const u8, len: usize) -> Vec { +unsafe fn read_bytes(ptr: *const u8, len: u32) -> Vec { + let len = len as usize; let mut data = Vec::with_capacity(len); memcpy(ptr, data.as_mut_ptr(), len); data.set_len(len); diff --git a/replay/src/trace.rs b/replay/src/trace.rs index 37fc656..f7b9bb2 100644 --- a/replay/src/trace.rs +++ b/replay/src/trace.rs @@ -3,7 +3,7 @@ #![allow(clippy::redundant_closure_call)] -use alloy_primitives::{Address, FixedBytes, TxHash, B256, U256}; +use alloy_primitives::{Address, TxHash, B256, U256}; use cargo_stylus_util::color::{Color, DebugColor}; use ethers::{ providers::{JsonRpcClient, Middleware, Provider}, @@ -205,11 +205,6 @@ impl TraceFrame { read_num!($src, u64) }; } - macro_rules! read_usize { - ($src:ident) => { - read_num!($src, usize) - }; - } macro_rules! frame { () => {{ @@ -241,14 +236,13 @@ impl TraceFrame { "write_result" => WriteResult { result: read_data!(args), }, + "exit_early" => ExitEarly { + status: read_u32!(args), + }, "storage_load_bytes32" => StorageLoadBytes32 { key: read_b256!(args), value: read_b256!(outs), }, - "storage_store_bytes32" => StorageStoreBytes32 { - key: read_b256!(args), - value: read_b256!(args), - }, "storage_cache_bytes32" => StorageCacheBytes32 { key: read_b256!(args), value: read_b256!(args), @@ -256,10 +250,28 @@ impl TraceFrame { "storage_flush_cache" => StorageFlushCache { clear: read_u8!(args), }, + "transient_load_bytes32" => TransientLoadBytes32 { + key: read_b256!(args), + value: read_b256!(outs), + }, + "transient_store_bytes32" => TransientStoreBytes32 { + key: read_b256!(args), + value: read_b256!(args), + }, "account_balance" => AccountBalance { address: read_address!(args), balance: read_u256!(outs), }, + "account_code" => AccountCode { + address: read_address!(args), + offset: read_u32!(args), + size: read_u32!(args), + code: read_data!(outs), + }, + "account_code_size" => AccountCodeSize { + address: read_address!(args), + size: read_u32!(outs), + }, "account_codehash" => AccountCodehash { address: read_address!(args), codehash: read_b256!(outs), @@ -291,6 +303,33 @@ impl TraceFrame { "evm_ink_left" => EvmInkLeft { ink_left: read_u64!(outs), }, + "math_div" => MathDiv { + a: read_u256!(args), + b: read_u256!(args), + result: read_u256!(outs), + }, + "math_mod" => MathMod { + a: read_u256!(args), + b: read_u256!(args), + result: read_u256!(outs), + }, + "math_pow" => MathPow { + a: read_u256!(args), + b: read_u256!(args), + result: read_u256!(outs), + }, + "math_add_mod" => MathAddMod { + a: read_u256!(args), + b: read_u256!(args), + c: read_u256!(args), + result: read_u256!(outs), + }, + "math_mul_mod" => MathMulMod { + a: read_u256!(args), + b: read_u256!(args), + c: read_u256!(args), + result: read_u256!(outs), + }, "msg_reentrant" => MsgReentrant { reentrant: read_u32!(outs) != 0, }, @@ -313,7 +352,7 @@ impl TraceFrame { "tx_origin" => TxOrigin { origin: read_address!(outs), }, - "memory_grow" => PayForMemoryGrow { + "pay_for_memory_grow" => PayForMemoryGrow { pages: read_u16!(args), }, "call_contract" => CallContract { @@ -345,17 +384,17 @@ impl TraceFrame { endowment: read_u256!(args), code: read_data!(args), address: read_address!(outs), - revert_data_len: read_usize!(outs), + revert_data_len: read_u32!(outs), }, "create2" => Create2 { endowment: read_u256!(args), salt: read_b256!(args), code: read_data!(args), address: read_address!(outs), - revert_data_len: read_usize!(outs), + revert_data_len: read_u32!(outs), }, "emit_log" => EmitLog { - topics: read_usize!(args), + topics: read_u32!(args), data: read_data!(args), }, "read_return_data" => ReadReturnData { @@ -364,7 +403,7 @@ impl TraceFrame { data: read_data!(outs), }, "return_data_size" => ReturnDataSize { - size: read_usize!(outs), + size: read_u32!(outs), }, "console_log_text" => ConsoleLogText { text: read_data!(args), @@ -410,25 +449,42 @@ pub enum HostioKind { WriteResult { result: Box<[u8]>, }, + ExitEarly { + status: u32, + }, StorageLoadBytes32 { key: B256, value: B256, }, - StorageStoreBytes32 { + StorageCacheBytes32 { key: B256, value: B256, }, - StorageCacheBytes32 { - key: FixedBytes<32>, - value: FixedBytes<32>, - }, StorageFlushCache { clear: u8, }, + TransientLoadBytes32 { + key: B256, + value: B256, + }, + TransientStoreBytes32 { + key: B256, + value: B256, + }, AccountBalance { address: Address, balance: U256, }, + AccountCode { + address: Address, + offset: u32, + size: u32, + code: Box<[u8]>, + }, + AccountCodeSize { + address: Address, + size: u32, + }, AccountCodehash { address: Address, codehash: B256, @@ -463,6 +519,33 @@ pub enum HostioKind { PayForMemoryGrow { pages: u16, }, + MathDiv { + a: U256, + b: U256, + result: U256, + }, + MathMod { + a: U256, + b: U256, + result: U256, + }, + MathPow { + a: U256, + b: U256, + result: U256, + }, + MathAddMod { + a: U256, + b: U256, + c: U256, + result: U256, + }, + MathMulMod { + a: U256, + b: U256, + c: U256, + result: U256, + }, MsgReentrant { reentrant: bool, }, @@ -520,18 +603,18 @@ pub enum HostioKind { code: Box<[u8]>, endowment: U256, address: Address, - revert_data_len: usize, + revert_data_len: u32, }, Create2 { code: Box<[u8]>, endowment: U256, salt: B256, address: Address, - revert_data_len: usize, + revert_data_len: u32, }, EmitLog { data: Box<[u8]>, - topics: usize, + topics: u32, }, ReadReturnData { offset: u32, @@ -539,7 +622,7 @@ pub enum HostioKind { data: Box<[u8]>, }, ReturnDataSize { - size: usize, + size: u32, }, } @@ -583,7 +666,7 @@ impl FrameReader { let kind = hostio.kind; let name = kind.name(); match name { - "memory_grow" | "user_entrypoint" | "user_returned" => continue, + "pay_for_memory_grow" | "user_entrypoint" | "user_returned" => continue, _ => { detected(self, expected); println!("However, onchain there's a call to {name}. Are you sure this the right contract?\n");