From e2aca56bef4ab0ec065a5ab0618ae27cd6440bde Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Fri, 13 Dec 2024 14:41:55 +0100 Subject: [PATCH 01/11] feat: impl x-contract calls --- README.md | 3195 ++++++++++++++++- contract-derive/Cargo.toml | 1 + contract-derive/src/lib.rs | 165 +- erc20/src/lib.rs | 23 +- eth-riscv-runtime/src/call.rs | 21 + eth-riscv-runtime/src/lib.rs | 37 +- .../src/{types.rs => types/mapping.rs} | 0 eth-riscv-runtime/src/types/mod.rs | 3 + r55/rvemu.dtb | Bin 0 -> 1598 bytes r55/rvemu.dts | 88 + r55/src/exec.rs | 263 +- r55/src/gas.rs | 42 + r55/src/lib.rs | 11 +- r55/tests/e2e.rs | 40 +- 14 files changed, 3679 insertions(+), 210 deletions(-) create mode 100644 eth-riscv-runtime/src/call.rs rename eth-riscv-runtime/src/{types.rs => types/mapping.rs} (100%) create mode 100644 eth-riscv-runtime/src/types/mod.rs create mode 100644 r55/rvemu.dtb create mode 100644 r55/rvemu.dts create mode 100644 r55/src/gas.rs diff --git a/README.md b/README.md index f86613e..3c67e67 100644 --- a/README.md +++ b/README.md @@ -1,169 +1,3142 @@ -# R55 +i am trying to understand the r55 project. here u have an intro of what it does: +``` +R55 is an experimental Ethereum Execution Environment that seamlessly integrates RISCV smart contracts alongside traditional EVM smart contracts. This dual support operates over the same Ethereum state, and communication happens via ABI-encoded calls. + +On the high level, R55 enables the use of pure Rust smart contracts, opening the door for a vast Rust developer community to engage in Ethereum development with minimal barriers to entry, and increasing language and compiler diversity. + +On the low level, RISCV code allows for optimization opportunities distinct from the EVM, including the use of off-the-shelf ASICs. This potential for performance gains can be particularly advantageous in specialized domains. +``` +and its architecture: +""" +# Architecture + +The compiler uses `rustc`, `llvm`, +[eth-riscv-syscalls](https://github.com/r55-eth/r55/tree/main/eth-riscv-syscalls), +[eth-riscv-runtime](https://github.com/r55-eth/r55/tree/main/eth-riscv-runtime) +and [riscv-rt](https://github.com/rust-embedded/riscv/tree/master/riscv-rt) to +compile and link ELF binaries with low-level syscalls to be executed by +[rvemu-r55](https://github.com/r55-eth/rvemu): + +```mermaid +graph TD; + RustContract[Rust contract] --> CompiledContract[compiled contract] + rustc --> CompiledContract + llvm --> CompiledContract + EthRiscVSyscalls[eth-riscv-syscalls] --> CompiledContract + EthRiscVRuntime1[eth-riscv-runtime] --> CompiledContract + CompiledContract --> LinkedRuntimeBytecode[linked runtime bytecode] + EthRiscVRuntime2[eth-riscv-runtime] --> LinkedRuntimeBytecode + riscv_rt[riscv-rt] --> LinkedRuntimeBytecode + LinkedRuntimeBytecode --> LinkedInitBytecode[linked init bytecode] + EthRiscVRuntime3[eth-riscv-runtime] --> LinkedInitBytecode +``` + +The execution environment depends on [revm](https://github.com/bluealloy/revm), +and relies on the [rvemu-r55](https://github.com/r55-eth/rvemu) RISCV +interpreter and +[eth-riscv-runtime](https://github.com/r55-eth/r55/tree/main/eth-riscv-runtime). + +```mermaid +graph TD; + revm --> revm-r55 + rvemu-r55 --> revm-r55 + eth-riscv-runtime --> revm-r55 +``` +""" + +i want to impl cross-contract call capabilities, and to do so, i first looked at the `contract-derive` crate, which converts the impl of a struct into a smart contract. +so i implemented a macro similar to the contract one, but to create interfaces from a trait, so that i could follow a similar approach for the cross-contract call capabilities. + +here you have the relevant code: +```contract-derive/src/lib.rs +extern crate proc_macro; +use alloy_core::primitives::keccak256; +use alloy_sol_types::SolValue; +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{parse_macro_input, Data, DeriveInput, Fields, ImplItem, ItemImpl, ItemTrait, TraitItem}; +use syn::{FnArg, ReturnType}; + +#[proc_macro_derive(Event, attributes(indexed))] +pub fn event_derive(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + + let fields = if let Data::Struct(data) = &input.data { + if let Fields::Named(fields) = &data.fields { + &fields.named + } else { + panic!("Event must have named fields"); + } + } else { + panic!("Event must be a struct"); + }; + + // Collect iterators into vectors + let field_names: Vec<_> = fields.iter().map(|f| &f.ident).collect(); + let field_types: Vec<_> = fields.iter().map(|f| &f.ty).collect(); + let indexed_fields: Vec<_> = fields + .iter() + .filter(|f| f.attrs.iter().any(|attr| attr.path.is_ident("indexed"))) + .map(|f| &f.ident) + .collect(); + + let expanded = quote! { + impl #name { + const NAME: &'static str = stringify!(#name); + const INDEXED_FIELDS: &'static [&'static str] = &[ + #(stringify!(#indexed_fields)),* + ]; + + pub fn new(#(#field_names: #field_types),*) -> Self { + Self { + #(#field_names),* + } + } + } + + impl eth_riscv_runtime::log::Event for #name { + fn encode_log(&self) -> (alloc::vec::Vec, alloc::vec::Vec<[u8; 32]>) { + use alloy_sol_types::SolValue; + use alloy_core::primitives::{keccak256, B256}; + use alloc::vec::Vec; + + let mut signature = Vec::new(); + signature.extend_from_slice(Self::NAME.as_bytes()); + signature.extend_from_slice(b"("); + + let mut first = true; + let mut topics = alloc::vec![B256::default()]; + let mut data = Vec::new(); + + #( + if !first { signature.extend_from_slice(b","); } + first = false; + + signature.extend_from_slice(self.#field_names.sol_type_name().as_bytes()); + let encoded = self.#field_names.abi_encode(); + + let field_name = stringify!(#field_names); + if Self::INDEXED_FIELDS.contains(&field_name) && topics.len() < 4 { + topics.push(B256::from_slice(&encoded)); + } else { + data.extend_from_slice(&encoded); + } + )* + + signature.extend_from_slice(b")"); + topics[0] = B256::from(keccak256(&signature)); + + (data, topics.iter().map(|t| t.0).collect()) + } + } + }; + + TokenStream::from(expanded) +} + +#[proc_macro_attribute] +pub fn show_streams(attr: TokenStream, item: TokenStream) -> TokenStream { + println!("attr: \"{}\"", attr.to_string()); + println!("item: \"{}\"", item.to_string()); + item +} + +#[proc_macro_attribute] +pub fn contract(_attr: TokenStream, item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as ItemImpl); + let struct_name = if let syn::Type::Path(type_path) = &*input.self_ty { + &type_path.path.segments.first().unwrap().ident + } else { + panic!("Expected a struct."); + }; + + let mut public_methods = Vec::new(); + + // Iterate over the items in the impl block to find pub methods + for item in input.items.iter() { + if let ImplItem::Method(method) = item { + if let syn::Visibility::Public(_) = method.vis { + public_methods.push(method.clone()); + } + } + } + + let match_arms: Vec<_> = public_methods.iter().enumerate().map(|(_, method)| { + let method_name = &method.sig.ident; + let method_selector = u32::from_be_bytes( + keccak256( + method_name.to_string() + )[..4].try_into().unwrap_or_default() + ); + let arg_types: Vec<_> = method.sig.inputs.iter().skip(1).map(|arg| { + if let FnArg::Typed(pat_type) = arg { + let ty = &*pat_type.ty; + quote! { #ty } + } else { + panic!("Expected typed arguments"); + } + }).collect(); + + let arg_names: Vec<_> = (0..method.sig.inputs.len() - 1).map(|i| format_ident!("arg{}", i)).collect(); + let checks = if !is_payable(&method) { + quote! { + if eth_riscv_runtime::msg_value() > U256::from(0) { + revert(); + } + } + } else { + quote! {} + }; + // Check if the method has a return type + let return_handling = match &method.sig.output { + ReturnType::Default => { + // No return value + quote! { + self.#method_name(#( #arg_names ),*); + } + } + ReturnType::Type(_, return_type) => { + // Has return value + quote! { + let result: #return_type = self.#method_name(#( #arg_names ),*); + let result_bytes = result.abi_encode(); + let result_size = result_bytes.len() as u64; + let result_ptr = result_bytes.as_ptr() as u64; + return_riscv(result_ptr, result_size); + } + } + }; + + quote! { + #method_selector => { + let (#( #arg_names ),*) = <(#( #arg_types ),*)>::abi_decode(calldata, true).unwrap(); + #checks + #return_handling + } + } + }).collect(); + + let emit_helper = quote! { + #[macro_export] + macro_rules! get_type_signature { + ($arg:expr) => { + $arg.sol_type_name().as_bytes() + }; + } + + #[macro_export] + macro_rules! emit { + ($event:ident, $($field:expr),*) => {{ + use alloy_sol_types::SolValue; + use alloy_core::primitives::{keccak256, B256, U256, I256}; + use alloc::vec::Vec; + + let mut signature = alloc::vec![]; + signature.extend_from_slice($event::NAME.as_bytes()); + signature.extend_from_slice(b"("); + + let mut first = true; + let mut topics = alloc::vec![B256::default()]; + let mut data = Vec::new(); + + $( + if !first { signature.extend_from_slice(b","); } + first = false; + + signature.extend_from_slice(get_type_signature!($field)); + let encoded = $field.abi_encode(); + + let field_ident = stringify!($field); + if $event::INDEXED_FIELDS.contains(&field_ident) && topics.len() < 4 { + topics.push(B256::from_slice(&encoded)); + } else { + data.extend_from_slice(&encoded); + } + )* + + signature.extend_from_slice(b")"); + topics[0] = B256::from(keccak256(&signature)); + + if !data.is_empty() { + eth_riscv_runtime::emit_log(&data, &topics); + } else if topics.len() > 1 { + let data = topics.pop().unwrap(); + eth_riscv_runtime::emit_log(data.as_ref(), &topics); + } + }}; + } + }; + + // Generate the call method implementation + let call_method = quote! { + use alloy_sol_types::SolValue; + use eth_riscv_runtime::*; + + #emit_helper + impl Contract for #struct_name { + fn call(&self) { + self.call_with_data(&msg_data()); + } + + fn call_with_data(&self, calldata: &[u8]) { + let selector = u32::from_be_bytes([calldata[0], calldata[1], calldata[2], calldata[3]]); + let calldata = &calldata[4..]; + + match selector { + #( #match_arms )* + _ => revert(), + } + + return_riscv(0, 0); + } + } + + #[eth_riscv_runtime::entry] + fn main() -> ! + { + let contract = #struct_name::default(); + contract.call(); + eth_riscv_runtime::return_riscv(0, 0) + } + }; + + let output = quote! { + #input + #call_method + }; + + TokenStream::from(output) +} + +// Empty macro to mark a method as payable +#[proc_macro_attribute] +pub fn payable(_attr: TokenStream, item: TokenStream) -> TokenStream { + item +} + +// Check if a method is tagged with the payable attribute +fn is_payable(method: &syn::ImplItemMethod) -> bool { + method.attrs.iter().any(|attr| { + if let Ok(syn::Meta::Path(path)) = attr.parse_meta() { + if let Some(segment) = path.segments.first() { + return segment.ident == "payable"; + } + } + false + }) +} -R55 is an experimental Ethereum Execution Environment that seamlessly -integrates RISCV smart contracts alongside traditional EVM smart contracts. -This dual support operates over the same Ethereum state, and communication -happens via ABI-encoded calls. +#[proc_macro_attribute] +pub fn interface(_attr: TokenStream, item: TokenStream) -> TokenStream { + // DEBUG + println!("\n=== Input Trait ==="); + println!("{}", item.to_string()); -On the high level, R55 enables the use of pure Rust smart contracts, opening -the door for a vast Rust developer community to engage in Ethereum development -with minimal barriers to entry, and increasing language and compiler diversity. + let input = parse_macro_input!(item as ItemTrait); + let trait_name = &input.ident; -On the low level, RISCV code allows for optimization opportunities distinct -from the EVM, including the use of off-the-shelf ASICs. This potential for -performance gains can be particularly advantageous in specialized domains. + let method_impls: Vec<_> = input + .items + .iter() + .map(|item| { + if let TraitItem::Method(method) = item { + let method_name = &method.sig.ident; + let selector_bytes = keccak256(method_name.to_string())[..4] + .try_into() + .unwrap_or_default(); + let method_selector = u32::from_be_bytes(selector_bytes); -# Off-the-shelf tooling + // DEBUG + println!("\n=== Processing Method ==="); + println!("Method name: {}", method_name); + println!("Selector bytes: {:02x?}", selector_bytes); + println!("Selector u32: {}", method_selector); -R55 relies on standard tooling that programmers are used to, such as Rust, -Cargo and LLVM. This directly enables tooling such as linters, static -analyzers, testing, fuzzing, and formal verification tools to be applied to -these smart contracts without extra development and research. + // Extract argument types and names, skipping self + let arg_types: Vec<_> = method + .sig + .inputs + .iter() + .skip(1) + .map(|arg| { + if let FnArg::Typed(pat_type) = arg { + let ty = &*pat_type.ty; + quote! { #ty } + } else { + panic!("Expected typed arguments"); + } + }) + .collect(); + let arg_names: Vec<_> = (0..method.sig.inputs.len() - 1) + .map(|i| format_ident!("arg{}", i)) + .collect(); -# Pure & Clean Rust Smart Contracts + // Get the return type + let return_type = match &method.sig.output { + ReturnType::Default => quote! { () }, + ReturnType::Type(_, ty) => + quote! { #ty }, + }; -Differently from other platforms that offer Rust smart contracts, R55 lets the -user code in [no\_std] Rust without weird edges. The code below implements a -basic ERC20 token with infinite minting for testing. -Because the `struct` and `impl` are just Rust code, the user can write normal -tests and run them natively (as long as they don't need Ethereum host -functions). Note that [alloy-rs](https://github.com/alloy-rs/) types work -out-of-the-box. + // Get the return size + let return_size = match &method.sig.output { + ReturnType::Default => quote! { 0_u64 }, + // TODO: Figure out how to use SolType to figure out the return size + ReturnType::Type(_, ty) => quote! {32_u64} -```rust + }; + + // Generate calldata with different encoding depending on # of args + let args_encoding = if arg_names.is_empty() { + quote! { + let mut complete_calldata = Vec::with_capacity(4); + complete_calldata.extend_from_slice(&[ + #method_selector.to_be_bytes()[0], + #method_selector.to_be_bytes()[1], + #method_selector.to_be_bytes()[2], + #method_selector.to_be_bytes()[3], + ]); + } + } else if arg_names.len() == 1 { + quote! { + let mut args_calldata = #(#arg_names),*.abi_encode(); + let mut complete_calldata = Vec::with_capacity(4 + args_calldata.len()); + complete_calldata.extend_from_slice(&[ + #method_selector.to_be_bytes()[0], + #method_selector.to_be_bytes()[1], + #method_selector.to_be_bytes()[2], + #method_selector.to_be_bytes()[3], + ]); + complete_calldata.append(&mut args_calldata); + } + } else { + quote! { + let mut args_calldata = (#(#arg_names),*).abi_encode(); + let mut complete_calldata = Vec::with_capacity(4 + args_calldata.len()); + complete_calldata.extend_from_slice(&[ + #method_selector.to_be_bytes()[0], + #method_selector.to_be_bytes()[1], + #method_selector.to_be_bytes()[2], + #method_selector.to_be_bytes()[3], + ]); + complete_calldata.append(&mut args_calldata); + } + }; + + Some(quote! { + pub fn #method_name(&self, #(#arg_names: #arg_types),*) -> Option<#return_type> { + use alloy_sol_types::SolValue; + use alloc::vec::Vec; + + #args_encoding + + // Make the call + let result = eth_riscv_runtime::call_contract( + self.address, + 0_u64, + &complete_calldata, + #return_size as u64 + )?; + + // Decode result + <#return_type>::abi_decode(&result, true).ok() + } + }) + } else { + panic!("Expected methods arguments"); + } + }) + .collect(); + + let expanded = quote! { + pub struct #trait_name { + address: Address, + } + + impl #trait_name { + pub fn new(address: Address) -> Self { + Self { address } + } + + #(#method_impls)* + } + }; + + // DEBUG + println!("\n=== Generated Code ==="); + println!("{:#}", expanded.to_string().replace(';', ";\n")); + + TokenStream::from(expanded) +} +``` + +thanks to those macros we can define a contract like: +```erc20/src/lib.rs #![no_std] #![no_main] use core::default::Default; -use contract_derive::contract; +use contract_derive::{contract, interface, payable, Event}; use eth_riscv_runtime::types::Mapping; -use alloy_core::primitives::Address; +use alloy_core::primitives::{address, Address, U256}; + +extern crate alloc; +use alloc::{string::String, vec::Vec}; #[derive(Default)] pub struct ERC20 { - balance: Mapping, + balances: Mapping, + allowances: Mapping>, + total_supply: U256, + name: String, + symbol: String, + decimals: u8, +} +#[derive(Event)] +pub struct DebugCalldata { + #[indexed] + pub target: Address, + pub calldata: Vec, +} + +#[interface] +trait IERC20 { + fn balance_of(&self, owner: Address) -> u64; +} + +#[derive(Event)] +pub struct Transfer { + #[indexed] + pub from: Address, + #[indexed] + pub to: Address, + pub value: u64, +} + +#[derive(Event)] +pub struct Mint { + #[indexed] + pub from: Address, + #[indexed] + pub to: Address, + pub value: u64, } #[contract] impl ERC20 { + pub fn x_balance_of(&self, owner: Address, target: Address) -> u64 { + let token = IERC20::new(target); + match token.balance_of(owner) { + Some(balance) => balance, + _ => eth_riscv_runtime::revert(), + } + } + pub fn balance_of(&self, owner: Address) -> u64 { - self.balance.read(owner) + self.balances.read(owner) } - pub fn transfer(&self, from: Address, to: Address, value: u64) { - let from_balance = self.balance.read(from); - let to_balance = self.balance.read(to); + pub fn transfer(&self, to: Address, value: u64) -> bool { + let from = msg_sender(); + let from_balance = self.balances.read(from); + let to_balance = self.balances.read(to); if from == to || from_balance < value { revert(); } - self.balance.write(from, from_balance - value); - self.balance.write(to, to_balance + value); + self.balances.write(from, from_balance - value); + self.balances.write(to, to_balance + value); + + log::emit(Transfer::new(from, to, value)); + true + } + + pub fn approve(&self, spender: Address, value: u64) -> bool { + let spender_allowances = self.allowances.read(msg_sender()); + spender_allowances.write(spender, value); + true + } + + pub fn transfer_from(&self, sender: Address, recipient: Address, amount: u64) -> bool { + let allowance = self.allowances.read(sender).read(msg_sender()); + let sender_balance = self.balances.read(sender); + let recipient_balance = self.balances.read(recipient); + + self.allowances + .read(sender) + .write(msg_sender(), allowance - amount); + self.balances.write(sender, sender_balance - amount); + self.balances.write(recipient, recipient_balance + amount); + + true + } + + pub fn total_supply(&self) -> U256 { + self.total_supply + } + + pub fn allowance(&self, owner: Address, spender: Address) -> u64 { + self.allowances.read(owner).read(spender) } - pub fn mint(&self, to: Address, value: u64) { - let to_balance = self.balance.read(to); - self.balance.write(to, to_balance + value); + #[payable] + pub fn mint(&self, to: Address, value: u64) -> bool { + let owner = msg_sender(); + + let to_balance = self.balances.read(to); + self.balances.write(to, to_balance + value); + log::emit(Transfer::new( + address!("0000000000000000000000000000000000000000"), + to, + value, + )); + true } } ``` -The macro `#[contract]` above is the only special treatment the user needs to -apply to their code. Specifically, it is responsible for the init code -(deployer), and for creating the function dispatcher based on the given -methods. -Note that Rust `pub` methods are exposed as public functions in the deployed -contract, similarly to Solidity's `public` functions. +I am quite happy with how this came out. However, this is just the API that developers will use to interact with other contracts. I still needed to implement the cross contract call capabilities. +To do so, r55 uses a hybrid execution environment leveraging risc-v when possible and via syscalls that use revm. + +Here u have the syscalls: +```eth-riscv-syscall/src/lib.rs +#![no_std] + +extern crate alloc; -# Client Integration +mod error; +pub use error::Error; -R55 is a fork of [revm](https://github.com/bluealloy/revm) without any API -changes. Therefore it can be used seamlessly in Anvil/Reth to deploy a -testnet/network with support to RISCV smart contracts. -Nothing has to be changed in how transactions are handled or created. +macro_rules! syscalls { + ($(($num:expr, $identifier:ident, $name:expr)),* $(,)?) => { + #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] + #[repr(u8)] + pub enum Syscall { + $($identifier = $num),* + } -# Relevant Links + impl core::fmt::Display for Syscall { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "{}", match self { + $(Syscall::$identifier => $name),* + }) + } + } -- [revm-R55](https://github.com/r0qs/revm) -- [rvemu-R55](https://github.com/r55-eth/rvemu) -- [R55 Ethereum Runtime](https://github.com/r55-eth/r55/tree/main/eth-riscv-runtime) -- [R55 Compiler](https://github.com/r55-eth/r55/tree/main/r55) + impl core::str::FromStr for Syscall { + type Err = Error; + fn from_str(input: &str) -> Result { + match input { + $($name => Ok(Syscall::$identifier)),*, + name => Err(Error::ParseError { input: alloc::string::String::from(name).into() }), + } + } + } -# Prerequisites + impl From for u8 { + fn from(syscall: Syscall) -> Self { + syscall as Self + } + } -## macOS + impl core::convert::TryFrom for Syscall { + type Error = Error; + fn try_from(value: u8) -> Result { + match value { + $($num => Ok(Syscall::$identifier)),*, + num => Err(Error::UnknownOpcode(num)), + } + } + } + } +} -```shell -brew install riscv-gnu-toolchain gettext +// Generate `Syscall` enum with supported syscalls and their ids. +// +// The opcode for each syscall matches the corresponding EVM opcode, +// as described on https://www.evm.codes. +// +// t0: 0x20, opcode for keccak256, a0: offset, a1: size, returns keccak256 hash +// t0: 0x32, opcode for origin, returns an address +// t0: 0x33, opcode for caller, returns an address +// t0: 0x34, opcode for callvalue, a0: first limb, a1: second limb, a2: third limb, a3: fourth limb, returns 256-bit value +// t0: 0x3A, opcode for gasprice, returns 256-bit value +// t0: 0x54, opcode for sload, a0: storage key, returns 64-bit value in a0 +// t0: 0x55, opcode for sstore, a0: storage key, a1: storage value, returns nothing +// t0: 0xf1, opcode for call, args: TODO +// t0: 0xf3, opcode for return, a0: memory address of data, a1: length of data in bytes, doesn't return +// t0: 0xfd, opcode for revert, doesn't return +syscalls!( + (0x20, Keccak256, "keccak256"), + (0x32, Origin, "origin"), + (0x33, Caller, "caller"), + (0x34, CallValue, "callvalue"), + (0x3A, GasPrice, "gasprice"), + (0x42, Timestamp, "timestamp"), + (0x43, Number, "number"), + (0x45, GasLimit, "gaslimit"), + (0x46, ChainId, "chainid"), + (0x48, BaseFee, "basefee"), + (0x54, SLoad, "sload"), + (0x55, SStore, "sstore"), + (0xf1, Call, "call"), + (0xf3, Return, "return"), + (0xfd, Revert, "revert"), + (0xA0, Log, "log"), +); ``` -# Test +and the runtime: +```eth-riscv-runtime/src/lib.rs +#![no_std] +#![no_main] +#![feature(alloc_error_handler, maybe_uninit_write_slice, round_char_boundary)] -The [R55](https://github.com/r55-eth/r55/tree/main/r55) crate has an [e2e test](https://github.com/r55-eth/r55/tree/main/r55/tests/e2e.rs) -that puts everything together in an end-to-end PoC, compiling the -[erc20](https://github.com/r55-eth/r55/tree/main/erc20) contract, deploying -it to an internal instance of [revm-r55](https://github.com/r0qs/revm), and -running two transactions on it, first a `mint` then a `balance_of` check. +use alloc::vec::Vec; +use alloy_core::primitives::{Address, Bytes, B256, U256}; +use core::arch::asm; +use core::panic::PanicInfo; +use core::slice; +pub use riscv_rt::entry; -You'll need to install Rust's RISCV toolchain: +mod alloc; +pub mod block; +pub mod tx; +pub mod types; -```console -$ rustup install nightly-2024-02-01-x86_64-unknown-linux-gnu -``` +pub mod log; +pub use log::{emit_log, Event}; -Now run: +pub mod call; +pub use call::call_contract; -```console -$ cargo test --package r55 --test e2e -- erc20 --exact --show-output -... -Compiling deploy: erc20 -Cargo command completed successfully -Deployed at addr: 0x522b3294e6d06aa25ad0f1b8891242e335d3b459 -Tx result: 0x -Tx result: 0x000000000000000000000000000000000000000000000000000000000000002a -``` +const CALLDATA_ADDRESS: usize = 0x8000_0000; -First R55 compiles the runtime RISCV-ELF binary that will be deployed. This is -needed to also compile the initcode RISCV-ELF binary that runs the constructor -and creates the contract. -The `mint` function has no return values, seen in `Tx result: 0x`. We minted 42 -tokens to our test account in the first transaction, and we can see in the -second transaction that indeed the balance is 42 (0x2a). +pub trait Contract { + fn call(&self); + fn call_with_data(&self, calldata: &[u8]); +} -# Architecture +pub unsafe fn slice_from_raw_parts(address: usize, length: usize) -> &'static [u8] { + slice::from_raw_parts(address as *const u8, length) +} -The compiler uses `rustc`, `llvm`, -[eth-riscv-syscalls](https://github.com/r55-eth/r55/tree/main/eth-riscv-syscalls), -[eth-riscv-runtime](https://github.com/r55-eth/r55/tree/main/eth-riscv-runtime) -and [riscv-rt](https://github.com/rust-embedded/riscv/tree/master/riscv-rt) to -compile and link ELF binaries with low-level syscalls to be executed by -[rvemu-r55](https://github.com/r55-eth/rvemu): +#[panic_handler] +unsafe fn panic(_panic: &PanicInfo<'_>) -> ! { + static mut IS_PANICKING: bool = false; -```mermaid -graph TD; - RustContract[Rust contract] --> CompiledContract[compiled contract] - rustc --> CompiledContract - llvm --> CompiledContract - EthRiscVSyscalls[eth-riscv-syscalls] --> CompiledContract - EthRiscVRuntime1[eth-riscv-runtime] --> CompiledContract - CompiledContract --> LinkedRuntimeBytecode[linked runtime bytecode] - EthRiscVRuntime2[eth-riscv-runtime] --> LinkedRuntimeBytecode - riscv_rt[riscv-rt] --> LinkedRuntimeBytecode - LinkedRuntimeBytecode --> LinkedInitBytecode[linked init bytecode] - EthRiscVRuntime3[eth-riscv-runtime] --> LinkedInitBytecode + if !IS_PANICKING { + IS_PANICKING = true; + + revert(); + // TODO with string + //print!("{panic}\n"); + } else { + revert(); + // TODO with string + //print_str("Panic handler has panicked! Things are very dire indeed...\n"); + } +} + +use eth_riscv_syscalls::Syscall; + +pub fn return_riscv(addr: u64, offset: u64) -> ! { + unsafe { + asm!("ecall", in("a0") addr, in("a1") offset, in("t0") u8::from(Syscall::Return)); + } + unreachable!() +} + +pub fn sload(key: u64) -> U256 { + let first: u64; + let second: u64; + let third: u64; + let fourth: u64; + unsafe { + asm!("ecall", lateout("a0") first, lateout("a1") second, lateout("a2") third, lateout("a3") fourth, in("a0") key, in("t0") u8::from(Syscall::SLoad)); + } + U256::from_limbs([first, second, third, fourth]) +} + +pub fn sstore(key: u64, value: U256) { + let limbs = value.as_limbs(); + unsafe { + asm!("ecall", in("a0") key, in("a1") limbs[0], in("a2") limbs[1], in("a3") limbs[2], in("a4") limbs[3], in("t0") u8::from(Syscall::SStore)); + } +} + +pub fn call( + addr: Address, + value: u64, + data_offset: u64, + data_size: u64, + res_offset: u64, + res_size: u64, +) { + let addr: U256 = addr.into_word().into(); + let addr = addr.as_limbs(); + unsafe { + asm!( + "ecall", + in("a0") addr[0], + in("a1") addr[1], + in("a2") addr[2], + in("a3") value, + in("a4") data_offset, + in("a5") data_size, + in("a6") res_offset, + in("a7") res_size, + in("t0") u8::from(Syscall::Call) + ); + } +} + +pub fn revert() -> ! { + unsafe { + asm!("ecall", in("t0") u8::from(Syscall::Revert)); + } + unreachable!() +} + +pub fn keccak256(offset: u64, size: u64) -> B256 { + let first: u64; + let second: u64; + let third: u64; + let fourth: u64; + + unsafe { + asm!( + "ecall", + in("a0") offset, + in("a1") size, + lateout("a0") first, + lateout("a1") second, + lateout("a2") third, + lateout("a3") fourth, + in("t0") u8::from(Syscall::Keccak256) + ); + } + + let mut bytes = [0u8; 32]; + + bytes[0..8].copy_from_slice(&first.to_be_bytes()); + bytes[8..16].copy_from_slice(&second.to_be_bytes()); + bytes[16..24].copy_from_slice(&third.to_be_bytes()); + bytes[24..32].copy_from_slice(&fourth.to_be_bytes()); + + B256::from_slice(&bytes) +} + +pub fn msg_sender() -> Address { + let first: u64; + let second: u64; + let third: u64; + unsafe { + asm!("ecall", lateout("a0") first, lateout("a1") second, lateout("a2") third, in("t0") u8::from(Syscall::Caller)); + } + let mut bytes = [0u8; 20]; + bytes[0..8].copy_from_slice(&first.to_be_bytes()); + bytes[8..16].copy_from_slice(&second.to_be_bytes()); + bytes[16..20].copy_from_slice(&third.to_be_bytes()[..4]); + Address::from_slice(&bytes) +} + +pub fn msg_value() -> U256 { + let first: u64; + let second: u64; + let third: u64; + let fourth: u64; + unsafe { + asm!("ecall", lateout("a0") first, lateout("a1") second, lateout("a2") third, lateout("a3") fourth, in("t0") u8::from(Syscall::CallValue)); + } + U256::from_limbs([first, second, third, fourth]) +} + +pub fn msg_sig() -> [u8; 4] { + let sig = unsafe { slice_from_raw_parts(CALLDATA_ADDRESS + 8, 4) }; + sig.try_into().unwrap() +} + +pub fn msg_data() -> &'static [u8] { + let length = unsafe { slice_from_raw_parts(CALLDATA_ADDRESS, 8) }; + let length = u64::from_le_bytes([ + length[0], length[1], length[2], length[3], length[4], length[5], length[6], length[7], + ]) as usize; + unsafe { slice_from_raw_parts(CALLDATA_ADDRESS + 8, length) } +} + +pub fn log(data_ptr: u64, data_size: u64, topics_ptr: u64, topics_size: u64) { + unsafe { + asm!( + "ecall", + in("a0") data_ptr, + in("a1") data_size, + in("a2") topics_ptr, + in("a3") topics_size, + in("t0") u8::from(Syscall::Log) + ); + } +} + +#[allow(non_snake_case)] +#[no_mangle] +fn DefaultHandler() { + revert(); +} + +#[allow(non_snake_case)] +#[no_mangle] +fn ExceptionHandler(_trap_frame: &riscv_rt::TrapFrame) -> ! { + revert(); +} + +pub fn call_contract(addr: Address, value: u64, data: &[u8], ret_size: u64) -> Option { + let mut ret_data = Vec::with_capacity(ret_size as usize); + + call( + addr, + value, + data.as_ptr() as u64, + data.len() as u64, + ret_data.as_ptr() as u64, + ret_size, + ); + + Some(Bytes::from(ret_data)) +} ``` -The execution environment depends on [revm](https://github.com/bluealloy/revm), -and relies on the [rvemu-r55](https://github.com/r55-eth/rvemu) RISCV -interpreter and -[eth-riscv-runtime](https://github.com/r55-eth/r55/tree/main/eth-riscv-runtime). +And finally, where the magic happens is in the r55 crate, where we merge the riscv runtime with revm: +```r55/src/exec.rs +use alloy_core::primitives::Keccak256; +use core::{cell::RefCell, ops::Range}; +use eth_riscv_interpreter::setup_from_elf; +use eth_riscv_syscalls::Syscall; +use revm::{ + handler::register::EvmHandler, + interpreter::{ + CallInputs, CallScheme, CallValue, Host, InstructionResult, Interpreter, InterpreterAction, + InterpreterResult, SharedMemory, + }, + primitives::{address, Address, Bytes, ExecutionResult, Log, Output, TransactTo, B256, U256}, + Database, Evm, Frame, FrameOrResult, InMemoryDB, +}; +use rvemu::{emulator::Emulator, exception::Exception}; +use std::{collections::BTreeMap, rc::Rc, sync::Arc}; -```mermaid -graph TD; - revm --> revm-r55 - rvemu-r55 --> revm-r55 - eth-riscv-runtime --> revm-r55 +use super::error::{Error, Result, TxResult}; + +pub fn deploy_contract(db: &mut InMemoryDB, bytecode: Bytes) -> Result
{ + let mut evm = Evm::builder() + .with_db(db) + .modify_tx_env(|tx| { + tx.caller = address!("000000000000000000000000000000000000000A"); + tx.transact_to = TransactTo::Create; + tx.data = bytecode; + tx.value = U256::from(0); + }) + .append_handler_register(handle_register) + .build(); + evm.cfg_mut().limit_contract_code_size = Some(usize::MAX); + + let result = evm.transact_commit()?; + + match result { + ExecutionResult::Success { + output: Output::Create(_value, Some(addr)), + .. + } => { + println!("Deployed at addr: {:?}", addr); + Ok(addr) + } + result => Err(Error::UnexpectedExecResult(result)), + } +} + +pub fn run_tx(db: &mut InMemoryDB, addr: &Address, calldata: Vec) -> Result { + let mut evm = Evm::builder() + .with_db(db) + .modify_tx_env(|tx| { + tx.caller = address!("000000000000000000000000000000000000000A"); + tx.transact_to = TransactTo::Call(*addr); + tx.data = calldata.into(); + tx.value = U256::from(0); + tx.gas_price = U256::from(42); + tx.gas_limit = 100_000; + }) + .append_handler_register(handle_register) + .build(); + + let result = evm.transact_commit()?; + + match result { + ExecutionResult::Success { + reason: _, + gas_used, + gas_refunded: _, + logs, + output: Output::Call(value), + .. + } => { + println!("Tx result: {:?}", value); + Ok(TxResult { + output: value.into(), + logs, + gas_used, + status: true, + }) + } + result => Err(Error::UnexpectedExecResult(result)), + } +} + +#[derive(Debug)] +struct RVEmu { + emu: Emulator, + returned_data_destiny: Option>, + evm_gas: u64, +} + +fn riscv_context(frame: &Frame) -> Option { + let interpreter = frame.interpreter(); + + println!("Creating RISC-V context:"); + println!("Contract address: {}", interpreter.contract.target_address); + println!("Input size: {}", interpreter.contract.input.len()); + + let Some((0xFF, bytecode)) = interpreter.bytecode.split_first() else { + return None; + }; + println!("RISC-V bytecode size: {}", bytecode.len()); + + match setup_from_elf(bytecode, &interpreter.contract.input) { + Ok(emu) => { + println!( + "RISC-V emulator setup successfully with entry point: 0x{:x}", + emu.cpu.pc + ); + Some(RVEmu { + emu, + returned_data_destiny: None, + evm_gas: 0, + }) + } + Err(err) => { + println!("Failed to setup from ELF: {err}"); + None + } + } +} + +pub fn handle_register(handler: &mut EvmHandler<'_, EXT, DB>) { + let call_stack = Rc::>>::new(RefCell::new(Vec::new())); + + // create a riscv context on call frame. + let call_stack_inner = call_stack.clone(); + let old_handle = handler.execution.call.clone(); + handler.execution.call = Arc::new(move |ctx, inputs| { + let result = old_handle(ctx, inputs); + if let Ok(FrameOrResult::Frame(frame)) = &result { + println!("----"); + println!("Frame created successfully"); + println!("Contract: {}", frame.interpreter().contract.target_address); + println!("Code size: {}", frame.interpreter().bytecode.len()); + + let context = riscv_context(frame); + println!("RISC-V context created: {}", context.is_some()); + println!("----"); + + call_stack_inner.borrow_mut().push(context); + } + result + }); + + // create a riscv context on create frame. + let call_stack_inner = call_stack.clone(); + let old_handle = handler.execution.create.clone(); + handler.execution.create = Arc::new(move |ctx, inputs| { + let result = old_handle(ctx, inputs); + if let Ok(FrameOrResult::Frame(frame)) = &result { + call_stack_inner.borrow_mut().push(riscv_context(frame)); + } + result + }); + + // execute riscv context or old logic. + let old_handle = handler.execution.execute_frame.clone(); + handler.execution.execute_frame = Arc::new(move |frame, memory, instraction_table, ctx| { + let depth = call_stack.borrow().len() - 1; + let result = if let Some(Some(riscv_context)) = call_stack.borrow_mut().last_mut() { + println!( + "\n*[{}] RISC-V Emu Handler (with PC: 0x{:x})", + depth, riscv_context.emu.cpu.pc + ); + + execute_riscv(riscv_context, frame.interpreter_mut(), memory, ctx)? + } else { + println!("\n*[OLD Handler]"); + old_handle(frame, memory, instraction_table, ctx)? + }; + + // if it is return pop the stack. + if result.is_return() { + println!("=== RETURN Frame ==="); + call_stack.borrow_mut().pop(); + println!( + "Popped frame from stack. Remaining frames: {}", + call_stack.borrow().len() + ); + + // if cross-contract call, copy return data into memory range expected by the parent + if !call_stack.borrow().is_empty() { + if let Some(Some(parent)) = call_stack.borrow_mut().last_mut() { + if let Some(return_range) = &parent.returned_data_destiny { + if let InterpreterAction::Return { result: res } = &result { + // Get allocated memory slice + let return_memory = parent + .emu + .cpu + .bus + .get_dram_slice(return_range.clone()) + .expect("unable to get memory from return range"); + + println!("Return data: {:?}", res.output); + println!("Memory range: {:?}", return_range); + println!("Memory size: {}", return_memory.len()); + + // Write return data to parent's memory + if res.output.len() == return_memory.len() { + println!("Copying output to memory"); + return_memory.copy_from_slice(&res.output); + } + + // Update gas spent + parent.evm_gas += res.gas.spent(); + } + } + } + } + } + + Ok(result) + }); +} + +fn execute_riscv( + rvemu: &mut RVEmu, + interpreter: &mut Interpreter, + shared_memory: &mut SharedMemory, + host: &mut dyn Host, +) -> Result { + println!( + "{} RISC-V execution:\n PC: {:#x}\n Contract: {}\n Return data dst: {:#?}", + if rvemu.emu.cpu.pc == 0x80300000 { + "Starting" + } else { + "Resuming" + }, + rvemu.emu.cpu.pc, + interpreter.contract.target_address, + &rvemu.returned_data_destiny + ); + + println!("interpreter remaining gas: {}", interpreter.gas.remaining()); + + let emu = &mut rvemu.emu; + emu.cpu.is_count = true; + + let returned_data_destiny = &mut rvemu.returned_data_destiny; + if let Some(destiny) = std::mem::take(returned_data_destiny) { + let data = emu.cpu.bus.get_dram_slice(destiny)?; + if shared_memory.len() >= data.len() { + data.copy_from_slice(shared_memory.slice(0, data.len())); + } + println!("Loaded return data: {}", Bytes::copy_from_slice(data)); + } + + let return_revert = |interpreter: &mut Interpreter, gas_used: u64| { + let _ = interpreter.gas.record_cost(gas_used); + Ok(InterpreterAction::Return { + result: InterpreterResult { + result: InstructionResult::Revert, + // return empty bytecode + output: Bytes::new(), + gas: interpreter.gas, + }, + }) + }; + + // Run emulator and capture ecalls + loop { + let run_result = emu.start(); + match run_result { + Err(Exception::EnvironmentCallFromMMode) => { + let t0: u64 = emu.cpu.xregs.read(5); + + let Ok(syscall) = Syscall::try_from(t0 as u8) else { + println!("Unhandled syscall: {:?}", t0); + return return_revert(interpreter, rvemu.evm_gas); + }; + println!("> [Syscall::{} - {:#04x}]", syscall, t0); + + match syscall { + Syscall::Return => { + let ret_offset: u64 = emu.cpu.xregs.read(10); + let ret_size: u64 = emu.cpu.xregs.read(11); + + let r55_gas = r55_gas_used(&emu.cpu.inst_counter); + let data_bytes = dram_slice(emu, ret_offset, ret_size)?; + + let total_cost = r55_gas + rvemu.evm_gas; + println!( + "evm gas: {}, r55 gas: {}, total cost: {}", + rvemu.evm_gas, r55_gas, total_cost + ); + let in_limit = interpreter.gas.record_cost(total_cost); + if !in_limit { + eprintln!("OUT OF GAS"); + return Ok(InterpreterAction::Return { + result: InterpreterResult { + result: InstructionResult::OutOfGas, + output: Bytes::new(), + gas: interpreter.gas, + }, + }); + } + + println!("interpreter remaining gas: {}", interpreter.gas.remaining()); + return Ok(InterpreterAction::Return { + result: InterpreterResult { + result: InstructionResult::Return, + output: data_bytes.to_vec().into(), + gas: interpreter.gas, // FIXME: gas is not correct + }, + }); + } + Syscall::SLoad => { + let key: u64 = emu.cpu.xregs.read(10); + println!( + "SLOAD ({}) - Key: {}", + interpreter.contract.target_address, key + ); + match host.sload(interpreter.contract.target_address, U256::from(key)) { + Some((value, is_cold)) => { + println!( + "SLOAD ({}) - Value: {}", + interpreter.contract.target_address, value + ); + let limbs = value.as_limbs(); + emu.cpu.xregs.write(10, limbs[0]); + emu.cpu.xregs.write(11, limbs[1]); + emu.cpu.xregs.write(12, limbs[2]); + emu.cpu.xregs.write(13, limbs[3]); + if is_cold { + rvemu.evm_gas += 2100 + } else { + rvemu.evm_gas += 100 + } + } + _ => { + return return_revert(interpreter, rvemu.evm_gas); + } + } + } + Syscall::SStore => { + let key: u64 = emu.cpu.xregs.read(10); + let first: u64 = emu.cpu.xregs.read(11); + let second: u64 = emu.cpu.xregs.read(12); + let third: u64 = emu.cpu.xregs.read(13); + let fourth: u64 = emu.cpu.xregs.read(14); + let result = host.sstore( + interpreter.contract.target_address, + U256::from(key), + U256::from_limbs([first, second, third, fourth]), + ); + if let Some(result) = result { + if result.is_cold { + rvemu.evm_gas += 2200 + } else { + rvemu.evm_gas += 100 + } + } + } + Syscall::Call => { + let a0: u64 = emu.cpu.xregs.read(10); + let a1: u64 = emu.cpu.xregs.read(11); + let a2: u64 = emu.cpu.xregs.read(12); + let addr = Address::from_word(U256::from_limbs([a0, a1, a2, 0]).into()); + let value: u64 = emu.cpu.xregs.read(13); + + // Get calldata + let args_offset: u64 = emu.cpu.xregs.read(14); + let args_size: u64 = emu.cpu.xregs.read(15); + let calldata: Bytes = emu + .cpu + .bus + .get_dram_slice(args_offset..(args_offset + args_size)) + .unwrap_or(&mut []) + .to_vec() + .into(); + + // Store where return data should go + let ret_offset = emu.cpu.xregs.read(16); + let ret_size = emu.cpu.xregs.read(17); + println!( + "Return data will be written to: {}..{}", + ret_offset, + ret_offset + ret_size + ); + + // Initialize memory region for return data + let return_memory = emu + .cpu + .bus + .get_dram_slice(ret_offset..(ret_offset + ret_size))?; + return_memory.fill(0); + rvemu.returned_data_destiny = Some(ret_offset..(ret_offset + ret_size)); + + // Calculate gas for the call + // TODO: Check correctness (tried using evm.codes as refjs) + let (empty_account_cost, addr_access_cost) = match host.load_account(addr) { + Some(account) => { + if account.is_cold { + (0, 2600) + } else { + (0, 100) + } + } + None => (25000, 2600), + }; + let value_cost = if value != 0 { 9000 } else { 0 }; + let call_gas_cost = empty_account_cost + addr_access_cost + value_cost; + rvemu.evm_gas += call_gas_cost; + + let r55_gas = r55_gas_used(&emu.cpu.inst_counter); + let total_cost = r55_gas + rvemu.evm_gas; + println!("Gas spent before call: {}", total_cost - call_gas_cost); + println!("Call gas cost {}", call_gas_cost); + // Pass remaining gas to the call + let tx = &host.env().tx; + let remaining_gas = tx.gas_limit.saturating_sub(total_cost); + println!("Remaining gas for call: {}", remaining_gas); + if !interpreter.gas.record_cost(total_cost) { + eprintln!("OUT OF GAS"); + }; + println!("interpreter remaining gas: {}", interpreter.gas.remaining()); + + println!("Call context:"); + println!(" Caller: {}", interpreter.contract.target_address); + println!(" Target Address: {}", addr); + println!(" Value: {}", value); + println!(" Call Gas limit: {}", remaining_gas); + println!(" Tx Gas limit: {}", tx.gas_limit); + println!(" Calldata: {:?}", calldata); + return Ok(InterpreterAction::Call { + inputs: Box::new(CallInputs { + input: calldata, + gas_limit: remaining_gas, + target_address: addr, + bytecode_address: addr, + caller: interpreter.contract.target_address, + value: CallValue::Transfer(U256::from(value)), + scheme: CallScheme::Call, + is_static: false, + is_eof: false, + return_memory_offset: 0..0, // We don't need this anymore + }), + }); + } + Syscall::Revert => { + return Ok(InterpreterAction::Return { + result: InterpreterResult { + result: InstructionResult::Revert, + output: Bytes::from(0u32.to_le_bytes()), //TODO: return revert(0,0) + gas: interpreter.gas, // FIXME: gas is not correct + }, + }); + } + Syscall::Caller => { + let caller = interpreter.contract.caller; + // Break address into 3 u64s and write to registers + let caller_bytes = caller.as_slice(); + let first_u64 = u64::from_be_bytes(caller_bytes[0..8].try_into()?); + emu.cpu.xregs.write(10, first_u64); + let second_u64 = u64::from_be_bytes(caller_bytes[8..16].try_into()?); + emu.cpu.xregs.write(11, second_u64); + let mut padded_bytes = [0u8; 8]; + padded_bytes[..4].copy_from_slice(&caller_bytes[16..20]); + let third_u64 = u64::from_be_bytes(padded_bytes); + emu.cpu.xregs.write(12, third_u64); + } + Syscall::Keccak256 => { + let ret_offset: u64 = emu.cpu.xregs.read(10); + let ret_size: u64 = emu.cpu.xregs.read(11); + let data_bytes = dram_slice(emu, ret_offset, ret_size)?; + + let mut hasher = Keccak256::new(); + hasher.update(data_bytes); + let hash: [u8; 32] = hasher.finalize().into(); + + // Write the hash to the emulator's registers + emu.cpu + .xregs + .write(10, u64::from_le_bytes(hash[0..8].try_into()?)); + emu.cpu + .xregs + .write(11, u64::from_le_bytes(hash[8..16].try_into()?)); + emu.cpu + .xregs + .write(12, u64::from_le_bytes(hash[16..24].try_into()?)); + emu.cpu + .xregs + .write(13, u64::from_le_bytes(hash[24..32].try_into()?)); + } + Syscall::CallValue => { + let value = interpreter.contract.call_value; + let limbs = value.into_limbs(); + emu.cpu.xregs.write(10, limbs[0]); + emu.cpu.xregs.write(11, limbs[1]); + emu.cpu.xregs.write(12, limbs[2]); + emu.cpu.xregs.write(13, limbs[3]); + } + Syscall::BaseFee => { + let value = host.env().block.basefee; + let limbs = value.as_limbs(); + emu.cpu.xregs.write(10, limbs[0]); + emu.cpu.xregs.write(11, limbs[1]); + emu.cpu.xregs.write(12, limbs[2]); + emu.cpu.xregs.write(13, limbs[3]); + } + Syscall::ChainId => { + let value = host.env().cfg.chain_id; + emu.cpu.xregs.write(10, value); + } + Syscall::GasLimit => { + let limit = host.env().block.gas_limit; + let limbs = limit.as_limbs(); + emu.cpu.xregs.write(10, limbs[0]); + emu.cpu.xregs.write(11, limbs[1]); + emu.cpu.xregs.write(12, limbs[2]); + emu.cpu.xregs.write(13, limbs[3]); + } + Syscall::Number => { + let number = host.env().block.number; + let limbs = number.as_limbs(); + emu.cpu.xregs.write(10, limbs[0]); + emu.cpu.xregs.write(11, limbs[1]); + emu.cpu.xregs.write(12, limbs[2]); + emu.cpu.xregs.write(13, limbs[3]); + } + Syscall::Timestamp => { + let timestamp = host.env().block.timestamp; + let limbs = timestamp.as_limbs(); + emu.cpu.xregs.write(10, limbs[0]); + emu.cpu.xregs.write(11, limbs[1]); + emu.cpu.xregs.write(12, limbs[2]); + emu.cpu.xregs.write(13, limbs[3]); + } + Syscall::GasPrice => { + let value = host.env().tx.gas_price; + let limbs = value.as_limbs(); + emu.cpu.xregs.write(10, limbs[0]); + emu.cpu.xregs.write(11, limbs[1]); + emu.cpu.xregs.write(12, limbs[2]); + emu.cpu.xregs.write(13, limbs[3]); + } + Syscall::Origin => { + // Syscall::Origin + let origin = host.env().tx.caller; + // Break address into 3 u64s and write to registers + let origin_bytes = origin.as_slice(); + + let first_u64 = u64::from_be_bytes(origin_bytes[0..8].try_into().unwrap()); + emu.cpu.xregs.write(10, first_u64); + + let second_u64 = + u64::from_be_bytes(origin_bytes[8..16].try_into().unwrap()); + emu.cpu.xregs.write(11, second_u64); + + let mut padded_bytes = [0u8; 8]; + padded_bytes[..4].copy_from_slice(&origin_bytes[16..20]); + let third_u64 = u64::from_be_bytes(padded_bytes); + emu.cpu.xregs.write(12, third_u64); + } + Syscall::Log => { + let data_ptr: u64 = emu.cpu.xregs.read(10); + let data_size: u64 = emu.cpu.xregs.read(11); + let topics_ptr: u64 = emu.cpu.xregs.read(12); + let topics_size: u64 = emu.cpu.xregs.read(13); + + // Read data + let data_slice = emu + .cpu + .bus + .get_dram_slice(data_ptr..(data_ptr + data_size)) + .unwrap_or(&mut []); + let data = data_slice.to_vec(); + + // Read topics + let topics_start = topics_ptr; + let topics_end = topics_ptr + topics_size * 32; + let topics_slice = emu + .cpu + .bus + .get_dram_slice(topics_start..topics_end) + .unwrap_or(&mut []); + let topics = topics_slice + .chunks(32) + .map(B256::from_slice) + .collect::>(); + + host.log(Log::new_unchecked( + interpreter.contract.target_address, + topics, + data.into(), + )); + } + } + } + Ok(_) => { + println!("Successful instruction at PC: {:#x}", emu.cpu.pc); + continue; + } + Err(e) => { + println!("Execution error: {:#?}", e); + let total_cost = r55_gas_used(&emu.cpu.inst_counter) + rvemu.evm_gas; + return return_revert(interpreter, total_cost); + } + } + } +} + +/// Returns RISC-V DRAM slice in a given size range, starts with a given offset +fn dram_slice(emu: &mut Emulator, ret_offset: u64, ret_size: u64) -> Result<&mut [u8]> { + if ret_size != 0 { + Ok(emu + .cpu + .bus + .get_dram_slice(ret_offset..(ret_offset + ret_size))?) + } else { + Ok(&mut []) + } +} + +fn r55_gas_used(inst_count: &BTreeMap) -> u64 { + let total_cost = inst_count + .iter() + .map(|(inst_name, count)| + // Gas cost = number of instructions * cycles per instruction + match inst_name.as_str() { + // Gas map to approximate cost of each instruction + // References: + // http://ithare.com/infographics-operation-costs-in-cpu-clock-cycles/ + // https://www.evm.codes/?fork=cancun#54 + // Division and remainder + s if s.starts_with("div") || s.starts_with("rem") => count * 25, + // Multiplications + s if s.starts_with("mul") => count * 5, + // Loads + "lb" | "lh" | "lw" | "ld" | "lbu" | "lhu" | "lwu" => count * 3, // Cost analagous to `MLOAD` + // Stores + "sb" | "sh" | "sw" | "sd" | "sc.w" | "sc.d" => count * 3, // Cost analagous to `MSTORE` + // Branching + "beq" | "bne" | "blt" | "bge" | "bltu" | "bgeu" | "jal" | "jalr" => count * 3, + _ => *count, // All other instructions including `add` and `sub` + }) + .sum::(); + + // This is the minimum 'gas used' to ABI decode 'empty' calldata into Rust type arguments. Real calldata will take more gas. + // Internalising this would focus gas metering more on the function logic + let abi_decode_cost = 9_175_538; + + total_cost - abi_decode_cost +} +``` + +as you can see, it is important to understand how revm works first, so here you have some context: +"""md + +# Evm Builder + +The builder creates or modifies the EVM and applies different handlers. +It allows setting external context and registering handler custom logic. + +The revm `Evm` consists of `Context` and `Handler`. +`Context` is additionally split between `EvmContext` (contains generic `Database`) and `External` context (generic without restrain). +Read [evm](./evm.md) for more information on the internals. + +The `Builder` ties dependencies between generic `Database`, `External` context and `Spec`. +It allows handle registers to be added that implement logic on those generics. +As they are interconnected, setting `Database` or `ExternalContext` resets handle registers, so builder stages are introduced to mitigate those misuses. + +Simple example of using `EvmBuilder`: + +```rust,ignore + use crate::evm::Evm; + + // build Evm with default values. + let mut evm = Evm::builder().build(); + let output = evm.transact(); +``` + +## Builder Stages + +There are two builder stages that are used to mitigate potential misuse of the builder: + * `SetGenericStage`: Initial stage that allows setting the database and external context. + * `HandlerStage`: Allows setting the handler registers but is explicit about setting new generic type as it will void the handler registers. + +Functions from one stage are just renamed functions from other stage, it is made so that user is more aware of what underlying function does. +For example, in `SettingDbStage` we have `with_db` function while in `HandlerStage` we have `reset_handler_with_db`, both of them set the database but the latter also resets the handler. +There are multiple functions that are common to both stages such as `build`. + +### Builder naming conventions +In both stages we have: + * `build` creates the Evm. + * `spec_id` creates new mainnet handler and reapplies all the handler registers. + * `modify_*` functions are used to modify the database, external context or Env. + * `clear_*` functions allows setting default values for Environment. + * `append_handler_register_*` functions are used to push handler registers. + This will transition the builder to the `HandlerStage`. + +In `SetGenericStage` we have: + * `with_*` are found in `SetGenericStage` and are used to set the generics. + +In `HandlerStage` we have: + * `reset_handler_with_*` is used if we want to change some of the generic types this will reset the handler registers. + This will transition the builder to the `SetGenericStage`. + +# Creating and modification of Evm + +Evm implements functions that allow using the `EvmBuilder` without even knowing that it exists. +The most obvious one is `Evm::builder()` that creates a new builder with default values. + +Additionally, a function that is very important is `evm.modify()` that allows modifying the Evm. +It returns a builder, allowing users to modify the Evm. + +# Examples +The following example uses the builder to create an `Evm` with inspector: +```rust,ignore + use crate::{ + db::EmptyDB, Context, EvmContext, inspector::inspector_handle_register, inspectors::NoOpInspector, Evm, + }; + + // Create the evm. + let evm = Evm::builder() + .with_db(EmptyDB::default()) + .with_external_context(NoOpInspector) + // Register will modify Handler and call NoOpInspector. + .append_handler_register(inspector_handle_register) + // .with_db(..) does not compile as we already locked the builder generics, + // alternative fn is reset_handler_with_db(..) + .build(); + + // Execute the evm. + let output = evm.transact(); + + // Extract evm context. + let Context { + external, + evm: EvmContext { db, .. }, + } = evm.into_context(); +``` + +The next example changes the spec id and environment of an already built evm. +```rust,ignore + use crate::{Evm,SpecId::BERLIN}; + + // Create default evm. + let evm = Evm::builder().build(); + + // Modify evm spec. + let evm = evm.modify().with_spec_id(BERLIN).build(); + + // Shortcut for above. + let mut evm = evm.modify_spec_id(BERLIN); + + // Execute the evm. + let output1 = evm.transact(); + + // Example of modifying the tx env. + let mut evm = evm.modify().modify_tx_env(|env| env.gas_price = 0.into()).build(); + + // Execute the evm with modified tx env. + let output2 = evm.transact(); +``` + +Example of adding custom precompiles to Evm. + +```rust,ignore +use super::SpecId; +use crate::{ + db::EmptyDB, + inspector::inspector_handle_register, + inspectors::NoOpInspector, + primitives::{Address, Bytes, ContextStatefulPrecompile, ContextPrecompile, PrecompileResult}, + Context, Evm, EvmContext, +}; +use std::sync::Arc; + +struct CustomPrecompile; + +impl ContextStatefulPrecompile, ()> for CustomPrecompile { + fn call( + &self, + _input: &Bytes, + _gas_limit: u64, + _context: &mut EvmContext, + _extcontext: &mut (), + ) -> PrecompileResult { + Ok((10, Bytes::new())) + } +} +fn main() { + let mut evm = Evm::builder() + .with_empty_db() + .with_spec_id(SpecId::HOMESTEAD) + .append_handler_register(|handler| { + let precompiles = handler.pre_execution.load_precompiles(); + handler.pre_execution.load_precompiles = Arc::new(move || { + let mut precompiles = precompiles.clone(); + precompiles.extend([( + Address::ZERO, + ContextPrecompile::ContextStateful(Arc::new(CustomPrecompile)), + )]); + precompiles + }); + }) + .build(); + + evm.transact().unwrap(); +} + +``` + +## Appending handler registers + +Handler registers are simple functions that allow modifying the `Handler` logic by replacing the handler functions. +They are used to add custom logic to the evm execution but as they are free to modify the `Handler` in any form they want. +There may be conflicts if handlers that override the same function are added. + +The most common use case for adding new logic to `Handler` is `Inspector` that is used to inspect the execution of the evm. +Example of this can be found in [`Inspector`](./inspector.md) documentation. +""" + +as you can see, `r55/src/exec.rs` impls `handle_register()` to use the risc-v instruction set. +it is also worth mentioning that (inside the `handle_register() fn`) i added this code to try to handle x-contract calls: +``` + // if cross-contract call, copy return data into memory range expected by the parent + if !call_stack.borrow().is_empty() { + if let Some(Some(parent)) = call_stack.borrow_mut().last_mut() { + if let Some(return_range) = &parent.returned_data_destiny { + if let InterpreterAction::Return { result: res } = &result { + // Get allocated memory slice + let return_memory = parent + .emu + .cpu + .bus + .get_dram_slice(return_range.clone()) + .expect("unable to get memory from return range"); + + println!("Return data: {:?}", res.output); + println!("Memory range: {:?}", return_range); + println!("Memory size: {}", return_memory.len()); + + // Write return data to parent's memory + if res.output.len() == return_memory.len() { + println!("Copying output to memory"); + return_memory.copy_from_slice(&res.output); + } + + // Update gas spent + parent.evm_gas += res.gas.spent(); + } + } + } + } +``` + +with all of that, you should have a good understanding of the project and what i'm trying to achieve. +so to finish, let me share the test suit that i'm using: + +```r55/tests/e2e.rs +use alloy_primitives::Bytes; +use alloy_sol_types::SolValue; +use r55::{ + compile_deploy, compile_with_prefix, + exec::{deploy_contract, run_tx}, + test_utils::{add_balance_to_db, get_selector_from_sig, initialize_logger}, +}; +use revm::{ + primitives::{address, Address}, + InMemoryDB, +}; + +const ERC20_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../erc20"); + +#[test] +fn erc20() { + initialize_logger(); + + let mut db = InMemoryDB::default(); + + let bytecode = compile_with_prefix(compile_deploy, ERC20_PATH).unwrap(); + let addr1 = deploy_contract(&mut db, bytecode.clone()).unwrap(); + let addr2 = deploy_contract(&mut db, bytecode).unwrap(); + + let selector_balance = get_selector_from_sig("balance_of"); + let selector_x_balance = get_selector_from_sig("x_balance_of"); + let selector_mint = get_selector_from_sig("mint"); + let alice: Address = address!("000000000000000000000000000000000000000A"); + let value_mint: u64 = 42; + let mut calldata_balance = alice.abi_encode(); + let mut calldata_mint = (alice, value_mint).abi_encode(); + let mut calldata_x_balance = (alice, addr1).abi_encode(); + + add_balance_to_db(&mut db, alice, 1e18 as u64); + + let mut complete_calldata_balance = selector_balance.to_vec(); + complete_calldata_balance.append(&mut calldata_balance); + + let mut complete_calldata_mint = selector_mint.to_vec(); + complete_calldata_mint.append(&mut calldata_mint); + + let mut complete_calldata_x_balance = selector_x_balance.to_vec(); + complete_calldata_x_balance.append(&mut calldata_x_balance); + + println!("\n----------------------------------------------------------"); + println!("-- MINT TX -----------------------------------------------"); + println!("----------------------------------------------------------"); + println!( + " > TX Calldata: {:#?\n}", + Bytes::from(complete_calldata_mint.clone()) + ); + run_tx(&mut db, &addr1, complete_calldata_mint.clone()).unwrap(); + println!("\n----------------------------------------------------------"); + println!("-- BALANCE OF TX -----------------------------------------"); + println!("----------------------------------------------------------"); + println!( + " > TX Calldata: {:#?}\n", + Bytes::from(complete_calldata_balance.clone()) + ); + run_tx(&mut db, &addr1, complete_calldata_balance.clone()).unwrap(); + println!("\n----------------------------------------------------------"); + println!("-- X-CONTRACT BALANCE OF TX ------------------------------"); + println!("----------------------------------------------------------"); + println!( + " > TX Calldata: {:#?}\n", + Bytes::from(complete_calldata_x_balance.clone()) + ); + match run_tx(&mut db, &addr2, complete_calldata_x_balance.clone()) { + Ok(res) => log::info!("res: {:#?}", res), + Err(e) => log::error!("{:#?}", e), + }; +} +``` + +and the logs that they output: +``` +---- erc20 stdout ---- +Creating RISC-V context: +Contract address: 0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d +Input size: 0 +RISC-V bytecode size: 96233 +RISC-V emulator setup successfully with entry point: 0x80300000 + +*[0] RISC-V Emu Handler (with PC: 0x80300000) +Starting RISC-V execution: + PC: 0x80300000 + Contract: 0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d + Return data dst: None +> [Syscall::return - 0xf3] +> total R55 gas: 131383 +> About to log gas costs: + - Operation cost: 131383 + - Gas remaining: 18446744073708358085 + - Gas limit: 18446744073708358085 + - Gas spent: 0 +> Gas recorded successfully: + - Gas remaining: 18446744073708358085 + - Gas spent: 131383 +interpreter remaining gas: 18446744073708226702 +=== RETURN Frame === +Popped frame from stack. Remaining frames: 0 +Deployed at addr: 0xf6a171f57acac30c292e223ea8adbb28abd3e14d +Creating RISC-V context: +Contract address: 0x114c28ea2CDe99e41D2566B1CfAA64a84f564caf +Input size: 0 +RISC-V bytecode size: 96233 +RISC-V emulator setup successfully with entry point: 0x80300000 + +*[0] RISC-V Emu Handler (with PC: 0x80300000) +Starting RISC-V execution: + PC: 0x80300000 + Contract: 0x114c28ea2CDe99e41D2566B1CfAA64a84f564caf + Return data dst: None +> [Syscall::return - 0xf3] +> total R55 gas: 131383 +> About to log gas costs: + - Operation cost: 131383 + - Gas remaining: 18446744073708358085 + - Gas limit: 18446744073708358085 + - Gas spent: 0 +> Gas recorded successfully: + - Gas remaining: 18446744073708358085 + - Gas spent: 131383 +interpreter remaining gas: 18446744073708226702 +=== RETURN Frame === +Popped frame from stack. Remaining frames: 0 +Deployed at addr: 0x114c28ea2cde99e41d2566b1cfaa64a84f564caf + +---------------------------------------------------------- +-- MINT TX ----------------------------------------------- +---------------------------------------------------------- + > TX Calldata: 0xdaf0b3c5000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000002a +---- +Frame created successfully +Contract: 0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d +Code size: 80970 +Creating RISC-V context: +Contract address: 0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d +Input size: 68 +RISC-V bytecode size: 80969 +RISC-V emulator setup successfully with entry point: 0x80300000 +RISC-V context created: true +---- + +*[0] RISC-V Emu Handler (with PC: 0x80300000) +Starting RISC-V execution: + PC: 0x80300000 + Contract: 0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d + Return data dst: None +> [Syscall::caller - 0x33] +> [Syscall::keccak256 - 0x20] +> [Syscall::sload - 0x54] +SLOAD (0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d) - Key: 2674963704631452285 +SLOAD (0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d) - Value: 0 +> About to log gas costs: + - Operation cost: 2100 + - Gas remaining: 78656 + - Gas limit: 78656 + - Gas spent: 0 +> Gas recorded successfully: + - Gas remaining: 78656 + - Gas spent: 2100 +> [Syscall::keccak256 - 0x20] +> [Syscall::sstore - 0x55] +> About to log gas costs: + - Operation cost: 100 + - Gas remaining: 76556 + - Gas limit: 78656 + - Gas spent: 2100 +> Gas recorded successfully: + - Gas remaining: 76556 + - Gas spent: 2200 +> [Syscall::log - 0xa0] +> [Syscall::return - 0xf3] +> total R55 gas: 24041 +> About to log gas costs: + - Operation cost: 24041 + - Gas remaining: 76456 + - Gas limit: 78656 + - Gas spent: 2200 +> Gas recorded successfully: + - Gas remaining: 76456 + - Gas spent: 26241 +interpreter remaining gas: 52415 +=== RETURN Frame === +Popped frame from stack. Remaining frames: 0 +Tx result: 0x0000000000000000000000000000000000000000000000000000000000000001 + +---------------------------------------------------------- +-- BALANCE OF TX ----------------------------------------- +---------------------------------------------------------- + > TX Calldata: 0xd35a73cd000000000000000000000000000000000000000000000000000000000000000a + +---- +Frame created successfully +Contract: 0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d +Code size: 80970 +Creating RISC-V context: +Contract address: 0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d +Input size: 36 +RISC-V bytecode size: 80969 +RISC-V emulator setup successfully with entry point: 0x80300000 +RISC-V context created: true +---- + +*[0] RISC-V Emu Handler (with PC: 0x80300000) +Starting RISC-V execution: + PC: 0x80300000 + Contract: 0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d + Return data dst: None +> [Syscall::callvalue - 0x34] +> [Syscall::keccak256 - 0x20] +> [Syscall::sload - 0x54] +SLOAD (0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d) - Key: 2674963704631452285 +SLOAD (0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d) - Value: 42 +> About to log gas costs: + - Operation cost: 2100 + - Gas remaining: 78796 + - Gas limit: 78796 + - Gas spent: 0 +> Gas recorded successfully: + - Gas remaining: 78796 + - Gas spent: 2100 +> [Syscall::return - 0xf3] +> total R55 gas: 4018 +> About to log gas costs: + - Operation cost: 4018 + - Gas remaining: 76696 + - Gas limit: 78796 + - Gas spent: 2100 +> Gas recorded successfully: + - Gas remaining: 76696 + - Gas spent: 6118 +interpreter remaining gas: 72678 +=== RETURN Frame === +Popped frame from stack. Remaining frames: 0 +Tx result: 0x000000000000000000000000000000000000000000000000000000000000002a + +---------------------------------------------------------- +-- X-CONTRACT BALANCE OF TX ------------------------------ +---------------------------------------------------------- + > TX Calldata: 0x51f59fb7000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000f6a171f57acac30c292e223ea8adbb28abd3e14d + +---- +Frame created successfully +Contract: 0x114c28ea2CDe99e41D2566B1CfAA64a84f564caf +Code size: 80970 +Creating RISC-V context: +Contract address: 0x114c28ea2CDe99e41D2566B1CfAA64a84f564caf +Input size: 68 +RISC-V bytecode size: 80969 +RISC-V emulator setup successfully with entry point: 0x80300000 +RISC-V context created: true +---- + +*[0] RISC-V Emu Handler (with PC: 0x80300000) +Starting RISC-V execution: + PC: 0x80300000 + Contract: 0x114c28ea2CDe99e41D2566B1CfAA64a84f564caf + Return data dst: None +> [Syscall::callvalue - 0x34] +> [Syscall::call - 0xf1] +Return data will be written to: 2150663548..2150663580 +> About to log gas costs: + - Operation cost: 2600 + - Gas remaining: 78428 + - Gas limit: 78428 + - Gas spent: 0 +> Gas recorded successfully: + - Gas remaining: 78428 + - Gas spent: 2600 +Call context: + Caller: 0x114c28ea2CDe99e41D2566B1CfAA64a84f564caf + Target Address: 0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d + Value: 0 + Calldata: 0xd35a73cd000000000000000000000000000000000000000000000000000000000000000a +---- +Frame created successfully +Contract: 0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d +Code size: 80970 +Creating RISC-V context: +Contract address: 0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d +Input size: 36 +RISC-V bytecode size: 80969 +RISC-V emulator setup successfully with entry point: 0x80300000 +RISC-V context created: true +---- + +*[1] RISC-V Emu Handler (with PC: 0x80300000) +Starting RISC-V execution: + PC: 0x80300000 + Contract: 0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d + Return data dst: None +> [Syscall::callvalue - 0x34] +> [Syscall::keccak256 - 0x20] +> [Syscall::sload - 0x54] +SLOAD (0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d) - Key: 2674963704631452285 +SLOAD (0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d) - Value: 42 +> About to log gas costs: + - Operation cost: 2100 + - Gas remaining: 75828 + - Gas limit: 75828 + - Gas spent: 0 +> Gas recorded successfully: + - Gas remaining: 75828 + - Gas spent: 2100 +> [Syscall::return - 0xf3] +> total R55 gas: 4018 +> About to log gas costs: + - Operation cost: 4018 + - Gas remaining: 73728 + - Gas limit: 75828 + - Gas spent: 2100 +> Gas recorded successfully: + - Gas remaining: 73728 + - Gas spent: 6118 +interpreter remaining gas: 69710 +=== RETURN Frame === +Popped frame from stack. Remaining frames: 1 +Return data: 0x000000000000000000000000000000000000000000000000000000000000002a +Memory range: 2150663548..2150663580 +Memory size: 32 +Copying output to memory + +*[0] RISC-V Emu Handler (with PC: 0x80300c5e) +Resuming RISC-V execution: + PC: 0x80300c5e + Contract: 0x114c28ea2CDe99e41D2566B1CfAA64a84f564caf + Return data dst: Some( + 2150663548..2150663580, +) +Loaded return data: 0x000000000000000000000000000000000000000000000000000000000000002a +> [Syscall::return - 0xf3] +> total R55 gas: 7553 +> About to log gas costs: + - Operation cost: 7553 + - Gas remaining: 145538 + - Gas limit: 78428 +thread 'erc20' panicked at /Users/rusowsky/.cargo/registry/src/index.crates.io-6f17d22bba15001f/revm-interpreter-5.0.0/src/gas.rs:66:9: +attempt to subtract with overflow +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +``` + +as u can see the cross-contract calls are almost ready! i am just facing some final issues with the gas calculations. To be more precise, the issue that i have is that the interpreter is refunding the whole unspent amount of each frame, making the "remaining" greater than the limit, which then creates an underflow. +i will share some of the relevant revm files to see if you can help me figure out how to fix the issue: + +> interpreter gas definition: in this case we want to look at `erase_cost()` fn +```revm/crates/interpreter/src/gas.rs +//! EVM gas calculation utilities. + +mod calc; +mod constants; + +pub use calc::*; +pub use constants::*; + +/// Represents the state of gas during execution. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Gas { + /// The initial gas limit. This is constant throughout execution. + limit: u64, + /// The remaining gas. + remaining: u64, + /// Refunded gas. This is used only at the end of execution. + refunded: i64, + /// Memoisation of values for memory expansion cost. + memory: MemoryGas, +} + +impl Gas { + /// Creates a new `Gas` struct with the given gas limit. + #[inline] + pub const fn new(limit: u64) -> Self { + Self { + limit, + remaining: limit, + refunded: 0, + memory: MemoryGas::new(), + } + } + + /// Creates a new `Gas` struct with the given gas limit, but without any gas remaining. + #[inline] + pub const fn new_spent(limit: u64) -> Self { + Self { + limit, + remaining: 0, + refunded: 0, + memory: MemoryGas::new(), + } + } + + /// Returns the gas limit. + #[inline] + pub const fn limit(&self) -> u64 { + self.limit + } + + /// Returns the **last** memory expansion cost. + #[inline] + #[deprecated = "memory expansion cost is not tracked anymore; \ + calculate it using `SharedMemory::current_expansion_cost` instead"] + #[doc(hidden)] + pub const fn memory(&self) -> u64 { + 0 + } + + /// Returns the total amount of gas that was refunded. + #[inline] + pub const fn refunded(&self) -> i64 { + self.refunded + } + + /// Returns the total amount of gas spent. + #[inline] + pub const fn spent(&self) -> u64 { + self.limit - self.remaining + } + + /// Returns the amount of gas remaining. + #[inline] + pub const fn remaining(&self) -> u64 { + self.remaining + } + + /// Return remaining gas after subtracting 63/64 parts. + pub const fn remaining_63_of_64_parts(&self) -> u64 { + self.remaining - self.remaining / 64 + } + + /// Erases a gas cost from the totals. + #[inline] + pub fn erase_cost(&mut self, returned: u64) { + self.remaining += returned; + } + + /// Spends all remaining gas. + #[inline] + pub fn spend_all(&mut self) { + self.remaining = 0; + } + + /// Records a refund value. + /// + /// `refund` can be negative but `self.refunded` should always be positive + /// at the end of transact. + #[inline] + pub fn record_refund(&mut self, refund: i64) { + self.refunded += refund; + } + + /// Set a refund value for final refund. + /// + /// Max refund value is limited to Nth part (depending of fork) of gas spend. + /// + /// Related to EIP-3529: Reduction in refunds + #[inline] + pub fn set_final_refund(&mut self, is_london: bool) { + let max_refund_quotient = if is_london { 5 } else { 2 }; + self.refunded = (self.refunded() as u64).min(self.spent() / max_refund_quotient) as i64; + } + + /// Set a refund value. This overrides the current refund value. + #[inline] + pub fn set_refund(&mut self, refund: i64) { + self.refunded = refund; + } + + /// Records an explicit cost. + /// + /// Returns `false` if the gas limit is exceeded. + #[inline] + #[must_use = "prefer using `gas!` instead to return an out-of-gas error on failure"] + pub fn record_cost(&mut self, cost: u64) -> bool { + let (remaining, overflow) = self.remaining.overflowing_sub(cost); + let success = !overflow; + if success { + self.remaining = remaining; + } + success + } + + /// Record memory expansion + #[inline] + #[must_use = "internally uses record_cost that flags out of gas error"] + pub fn record_memory_expansion(&mut self, new_len: usize) -> MemoryExtensionResult { + let Some(additional_cost) = self.memory.record_new_len(new_len) else { + return MemoryExtensionResult::Same; + }; + + if !self.record_cost(additional_cost) { + return MemoryExtensionResult::OutOfGas; + } + + MemoryExtensionResult::Extended + } +} + +pub enum MemoryExtensionResult { + /// Memory was extended. + Extended, + /// Memory size stayed the same. + Same, + /// Not enough gas to extend memory.s + OutOfGas, +} + +/// Utility struct that speeds up calculation of memory expansion +/// It contains the current memory length and its memory expansion cost. +/// +/// It allows us to split gas accounting from memory structure. +#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct MemoryGas { + /// Current memory length + pub words_num: usize, + /// Current memory expansion cost + pub expansion_cost: u64, +} + +impl MemoryGas { + pub const fn new() -> Self { + Self { + words_num: 0, + expansion_cost: 0, + } + } + + #[inline] + pub fn record_new_len(&mut self, new_num: usize) -> Option { + if new_num <= self.words_num { + return None; + } + self.words_num = new_num; + let mut cost = crate::gas::calc::memory_gas(new_num); + core::mem::swap(&mut self.expansion_cost, &mut cost); + // safe to subtract because we know that new_len > length + // notice the swap above. + Some(self.expansion_cost - cost) + } +} +``` + +> handler frame, in this case we want to look at `return_result()` fn: +```revm/crates/handler/src/frame.rs +use super::frame_data::*; +use bytecode::{Eof, EOF_MAGIC_BYTES}; +use context_interface::{ + journaled_state::{JournalCheckpoint, JournaledState}, + BlockGetter, Cfg, CfgGetter, ErrorGetter, JournalStateGetter, JournalStateGetterDBError, + Transaction, TransactionGetter, +}; +use core::{cell::RefCell, cmp::min}; +use handler_interface::{Frame, FrameOrResultGen, PrecompileProvider}; +use interpreter::{ + gas, + interpreter::{EthInterpreter, InstructionProvider}, + interpreter_types::{LoopControl, ReturnData, RuntimeFlag}, + return_ok, return_revert, CallInputs, CallOutcome, CallValue, CreateInputs, CreateOutcome, + CreateScheme, EOFCreateInputs, EOFCreateKind, FrameInput, Gas, Host, InputsImpl, + InstructionResult, Interpreter, InterpreterAction, InterpreterResult, InterpreterTypes, + SharedMemory, +}; +use precompile::PrecompileErrors; +use primitives::{keccak256, Address, Bytes, B256, U256}; +use specification::{ + constants::CALL_STACK_LIMIT, + hardfork::SpecId::{self, HOMESTEAD, LONDON, OSAKA, SPURIOUS_DRAGON}, +}; +use state::Bytecode; +use std::borrow::ToOwned; +use std::{rc::Rc, sync::Arc}; + +pub struct EthFrame { + _phantom: core::marker::PhantomData (CTX, ERROR)>, + data: FrameData, + // TODO include this + depth: usize, + /// Journal checkpoint. + pub checkpoint: JournalCheckpoint, + /// Interpreter. + pub interpreter: Interpreter, + /// Precompiles provider. + pub precompiles: PRECOMPILE, + /// Instruction provider. + pub instructions: INSTRUCTIONS, + // This is worth making as a generic type FrameSharedContext. + pub memory: Rc>, +} + +impl EthFrame +where + CTX: JournalStateGetter, + IW: InterpreterTypes, +{ + pub fn new( + data: FrameData, + depth: usize, + interpreter: Interpreter, + checkpoint: JournalCheckpoint, + precompiles: PRECOMP, + instructions: INST, + memory: Rc>, + ) -> Self { + Self { + _phantom: core::marker::PhantomData, + data, + depth, + interpreter, + checkpoint, + precompiles, + instructions, + memory, + } + } +} + +impl + EthFrame, PRECOMPILE, INSTRUCTION> +where + CTX: EthFrameContext, + ERROR: EthFrameError, + PRECOMPILE: PrecompileProvider, +{ + /// Make call frame + #[inline] + pub fn make_call_frame( + context: &mut CTX, + depth: usize, + memory: Rc>, + inputs: &CallInputs, + mut precompile: PRECOMPILE, + instructions: INSTRUCTION, + ) -> Result, ERROR> { + let gas = Gas::new(inputs.gas_limit); + + let return_result = |instruction_result: InstructionResult| { + Ok(FrameOrResultGen::Result(FrameResult::Call(CallOutcome { + result: InterpreterResult { + result: instruction_result, + gas, + output: Bytes::new(), + }, + memory_offset: inputs.return_memory_offset.clone(), + }))) + }; + + // Check depth + if depth > CALL_STACK_LIMIT as usize { + return return_result(InstructionResult::CallTooDeep); + } + + // Make account warm and loaded + let _ = context + .journal() + .load_account_delegated(inputs.bytecode_address)?; + + // Create subroutine checkpoint + let checkpoint = context.journal().checkpoint(); + + // Touch address. For "EIP-158 State Clear", this will erase empty accounts. + if let CallValue::Transfer(value) = inputs.value { + // Transfer value from caller to called account + // Target will get touched even if balance transferred is zero. + if let Some(i) = + context + .journal() + .transfer(&inputs.caller, &inputs.target_address, value)? + { + context.journal().checkpoint_revert(checkpoint); + return return_result(i.into()); + } + } + + if let Some(result) = precompile.run( + context, + &inputs.bytecode_address, + &inputs.input, + inputs.gas_limit, + )? { + if result.result.is_ok() { + context.journal().checkpoint_commit(); + } else { + context.journal().checkpoint_revert(checkpoint); + } + Ok(FrameOrResultGen::Result(FrameResult::Call(CallOutcome { + result, + memory_offset: inputs.return_memory_offset.clone(), + }))) + } else { + let account = context + .journal() + .load_account_code(inputs.bytecode_address)?; + + // TODO Request from foundry to get bytecode hash. + let _code_hash = account.info.code_hash(); + let mut bytecode = account.info.code.clone().unwrap_or_default(); + + // ExtDelegateCall is not allowed to call non-EOF contracts. + if inputs.scheme.is_ext_delegate_call() + && !bytecode.bytes_slice().starts_with(&EOF_MAGIC_BYTES) + { + return return_result(InstructionResult::InvalidExtDelegateCallTarget); + } + + if bytecode.is_empty() { + context.journal().checkpoint_commit(); + return return_result(InstructionResult::Stop); + } + + if let Bytecode::Eip7702(eip7702_bytecode) = bytecode { + bytecode = context + .journal() + .load_account_code(eip7702_bytecode.delegated_address)? + .info + .code + .clone() + .unwrap_or_default(); + } + + // Create interpreter and executes call and push new CallStackFrame. + let interpreter_input = InputsImpl { + target_address: inputs.target_address, + caller_address: inputs.caller, + input: inputs.input.clone(), + call_value: inputs.value.get(), + }; + + Ok(FrameOrResultGen::Frame(Self::new( + FrameData::Call(CallFrame { + return_memory_range: inputs.return_memory_offset.clone(), + }), + depth, + Interpreter::new( + memory.clone(), + bytecode, + interpreter_input, + inputs.is_static, + false, + context.cfg().spec().into(), + inputs.gas_limit, + ), + checkpoint, + precompile, + instructions, + memory, + ))) + } + } + + /// Make create frame. + #[inline] + pub fn make_create_frame( + context: &mut CTX, + depth: usize, + memory: Rc>, + inputs: &CreateInputs, + precompile: PRECOMPILE, + instructions: INSTRUCTION, + ) -> Result, ERROR> { + let spec = context.cfg().spec().into(); + let return_error = |e| { + Ok(FrameOrResultGen::Result(FrameResult::Create( + CreateOutcome { + result: InterpreterResult { + result: e, + gas: Gas::new(inputs.gas_limit), + output: Bytes::new(), + }, + address: None, + }, + ))) + }; + + // Check depth + if depth > CALL_STACK_LIMIT as usize { + return return_error(InstructionResult::CallTooDeep); + } + + // Prague EOF + if spec.is_enabled_in(OSAKA) && inputs.init_code.starts_with(&EOF_MAGIC_BYTES) { + return return_error(InstructionResult::CreateInitCodeStartingEF00); + } + + // Fetch balance of caller. + let caller_balance = context + .journal() + .load_account(inputs.caller)? + .map(|a| a.info.balance); + + // Check if caller has enough balance to send to the created contract. + if caller_balance.data < inputs.value { + return return_error(InstructionResult::OutOfFunds); + } + + // Increase nonce of caller and check if it overflows + let old_nonce; + if let Some(nonce) = context.journal().inc_account_nonce(inputs.caller)? { + old_nonce = nonce - 1; + } else { + return return_error(InstructionResult::Return); + } + + // Create address + // TODO incorporating code hash inside interpreter. It was a request by foundry. + let mut _init_code_hash = B256::ZERO; + let created_address = match inputs.scheme { + CreateScheme::Create => inputs.caller.create(old_nonce), + CreateScheme::Create2 { salt } => { + _init_code_hash = keccak256(&inputs.init_code); + inputs.caller.create2(salt.to_be_bytes(), _init_code_hash) + } + }; + + // created address is not allowed to be a precompile. + // TODO add precompile check + if precompile.contains(&created_address) { + return return_error(InstructionResult::CreateCollision); + } + + // warm load account. + context.journal().load_account(created_address)?; + + // create account, transfer funds and make the journal checkpoint. + let checkpoint = match context.journal().create_account_checkpoint( + inputs.caller, + created_address, + inputs.value, + spec, + ) { + Ok(checkpoint) => checkpoint, + Err(e) => return return_error(e.into()), + }; + + let bytecode = Bytecode::new_legacy(inputs.init_code.clone()); + + let interpreter_input = InputsImpl { + target_address: created_address, + caller_address: inputs.caller, + input: Bytes::new(), + call_value: inputs.value, + }; + + Ok(FrameOrResultGen::Frame(Self::new( + FrameData::Create(CreateFrame { created_address }), + depth, + Interpreter::new( + memory.clone(), + bytecode, + interpreter_input, + false, + false, + spec, + inputs.gas_limit, + ), + checkpoint, + precompile, + instructions, + memory, + ))) + } + + /// Make create frame. + #[inline] + pub fn make_eofcreate_frame( + context: &mut CTX, + depth: usize, + memory: Rc>, + inputs: &EOFCreateInputs, + precompile: PRECOMPILE, + instructions: INSTRUCTION, + ) -> Result, ERROR> { + let spec = context.cfg().spec().into(); + let return_error = |e| { + Ok(FrameOrResultGen::Result(FrameResult::EOFCreate( + CreateOutcome { + result: InterpreterResult { + result: e, + gas: Gas::new(inputs.gas_limit), + output: Bytes::new(), + }, + address: None, + }, + ))) + }; + + let (input, initcode, created_address) = match &inputs.kind { + EOFCreateKind::Opcode { + initcode, + input, + created_address, + } => (input.clone(), initcode.clone(), Some(*created_address)), + EOFCreateKind::Tx { initdata } => { + // decode eof and init code. + // TODO handle inc_nonce handling more gracefully. + let Ok((eof, input)) = Eof::decode_dangling(initdata.clone()) else { + context.journal().inc_account_nonce(inputs.caller)?; + return return_error(InstructionResult::InvalidEOFInitCode); + }; + + if eof.validate().is_err() { + // TODO (EOF) new error type. + context.journal().inc_account_nonce(inputs.caller)?; + return return_error(InstructionResult::InvalidEOFInitCode); + } + + // Use nonce from tx to calculate address. + let tx = context.tx().common_fields(); + let create_address = tx.caller().create(tx.nonce()); + + (input, eof, Some(create_address)) + } + }; + + // Check depth + if depth > CALL_STACK_LIMIT as usize { + return return_error(InstructionResult::CallTooDeep); + } + + // Fetch balance of caller. + let caller_balance = context + .journal() + .load_account(inputs.caller)? + .map(|a| a.info.balance); + + // Check if caller has enough balance to send to the created contract. + if caller_balance.data < inputs.value { + return return_error(InstructionResult::OutOfFunds); + } + + // Increase nonce of caller and check if it overflows + let Some(nonce) = context.journal().inc_account_nonce(inputs.caller)? else { + // can't happen on mainnet. + return return_error(InstructionResult::Return); + }; + let old_nonce = nonce - 1; + + let created_address = created_address.unwrap_or_else(|| inputs.caller.create(old_nonce)); + + // created address is not allowed to be a precompile. + if precompile.contains(&created_address) { + return return_error(InstructionResult::CreateCollision); + } + + // Load account so it needs to be marked as warm for access list. + context.journal().load_account(created_address)?; + + // create account, transfer funds and make the journal checkpoint. + let checkpoint = match context.journal().create_account_checkpoint( + inputs.caller, + created_address, + inputs.value, + spec, + ) { + Ok(checkpoint) => checkpoint, + Err(e) => return return_error(e.into()), + }; + + let interpreter_input = InputsImpl { + target_address: created_address, + caller_address: inputs.caller, + input, + call_value: inputs.value, + }; + + Ok(FrameOrResultGen::Frame(Self::new( + FrameData::EOFCreate(EOFCreateFrame { created_address }), + depth, + Interpreter::new( + memory.clone(), + Bytecode::Eof(Arc::new(initcode)), + interpreter_input, + false, + true, + spec, + inputs.gas_limit, + ), + checkpoint, + precompile, + instructions, + memory, + ))) + } + + pub fn init_with_context( + depth: usize, + frame_init: FrameInput, + memory: Rc>, + precompile: PRECOMPILE, + instructions: INSTRUCTION, + context: &mut CTX, + ) -> Result, ERROR> { + match frame_init { + FrameInput::Call(inputs) => { + Self::make_call_frame(context, depth, memory, &inputs, precompile, instructions) + } + FrameInput::Create(inputs) => { + Self::make_create_frame(context, depth, memory, &inputs, precompile, instructions) + } + FrameInput::EOFCreate(inputs) => Self::make_eofcreate_frame( + context, + depth, + memory, + &inputs, + precompile, + instructions, + ), + } + } +} + +impl Frame + for EthFrame, PRECOMPILE, INSTRUCTION> +where + CTX: EthFrameContext, + ERROR: EthFrameError, + PRECOMPILE: PrecompileProvider, + INSTRUCTION: InstructionProvider, Host = CTX>, +{ + type Context = CTX; + type Error = ERROR; + type FrameInit = FrameInput; + type FrameResult = FrameResult; + + fn init_first( + context: &mut Self::Context, + frame_input: Self::FrameInit, + ) -> Result, Self::Error> { + let memory = Rc::new(RefCell::new(SharedMemory::new())); + let precompiles = PRECOMPILE::new(context); + let instructions = INSTRUCTION::new(context); + + // load precompiles addresses as warm. + for address in precompiles.warm_addresses() { + context.journal().warm_account(address); + } + + memory.borrow_mut().new_context(); + Self::init_with_context(0, frame_input, memory, precompiles, instructions, context) + } + + fn init( + &self, + context: &mut CTX, + frame_init: Self::FrameInit, + ) -> Result, Self::Error> { + self.memory.borrow_mut().new_context(); + Self::init_with_context( + self.depth + 1, + frame_init, + self.memory.clone(), + self.precompiles.clone(), + self.instructions.clone(), + context, + ) + } + + fn run( + &mut self, + context: &mut Self::Context, + ) -> Result, Self::Error> { + let spec = context.cfg().spec().into(); + + // run interpreter + let next_action = self.interpreter.run(self.instructions.table(), context); + + let mut interpreter_result = match next_action { + InterpreterAction::NewFrame(new_frame) => { + return Ok(FrameOrResultGen::Frame(new_frame)) + } + InterpreterAction::Return { result } => result, + InterpreterAction::None => unreachable!("InterpreterAction::None is not expected"), + }; + + // Handle return from frame + let result = match &self.data { + FrameData::Call(frame) => { + // return_call + // revert changes or not. + if interpreter_result.result.is_ok() { + context.journal().checkpoint_commit(); + } else { + context.journal().checkpoint_revert(self.checkpoint); + } + FrameOrResultGen::Result(FrameResult::Call(CallOutcome::new( + interpreter_result, + frame.return_memory_range.clone(), + ))) + } + FrameData::Create(frame) => { + let max_code_size = context.cfg().max_code_size(); + return_create( + context.journal(), + self.checkpoint, + &mut interpreter_result, + frame.created_address, + max_code_size, + spec, + ); + + FrameOrResultGen::Result(FrameResult::Create(CreateOutcome::new( + interpreter_result, + Some(frame.created_address), + ))) + } + FrameData::EOFCreate(frame) => { + let max_code_size = context.cfg().max_code_size(); + return_eofcreate( + context.journal(), + self.checkpoint, + &mut interpreter_result, + frame.created_address, + max_code_size, + ); + + FrameOrResultGen::Result(FrameResult::EOFCreate(CreateOutcome::new( + interpreter_result, + Some(frame.created_address), + ))) + } + }; + + Ok(result) + } + + fn return_result( + &mut self, + context: &mut Self::Context, + result: Self::FrameResult, + ) -> Result<(), Self::Error> { + self.memory.borrow_mut().free_context(); + context.take_error()?; + + // Insert result to the top frame. + match result { + FrameResult::Call(outcome) => { + let out_gas = outcome.gas(); + let ins_result = *outcome.instruction_result(); + let returned_len = outcome.result.output.len(); + + let interpreter = &mut self.interpreter; + let mem_length = outcome.memory_length(); + let mem_start = outcome.memory_start(); + *interpreter.return_data.buffer_mut() = outcome.result.output; + + let target_len = min(mem_length, returned_len); + + if ins_result == InstructionResult::FatalExternalError { + panic!("Fatal external error in insert_call_outcome"); + } + + let item = { + if interpreter.runtime_flag.is_eof() { + match ins_result { + return_ok!() => U256::ZERO, + return_revert!() => U256::from(1), + _ => U256::from(2), + } + } else if ins_result.is_ok() { + U256::from(1) + } else { + U256::ZERO + } + }; + // Safe to push without stack limit check + let _ = interpreter.stack.push(item); + + // return unspend gas. + if ins_result.is_ok_or_revert() { + interpreter.control.gas().erase_cost(out_gas.remaining()); + self.memory + .borrow_mut() + .set(mem_start, &interpreter.return_data.buffer()[..target_len]); + } + + if ins_result.is_ok() { + interpreter.control.gas().record_refund(out_gas.refunded()); + } + } + FrameResult::Create(outcome) => { + let instruction_result = *outcome.instruction_result(); + let interpreter = &mut self.interpreter; + + let buffer = interpreter.return_data.buffer_mut(); + if instruction_result == InstructionResult::Revert { + // Save data to return data buffer if the create reverted + *buffer = outcome.output().to_owned() + } else { + // Otherwise clear it. Note that RETURN opcode should abort. + buffer.clear(); + }; + + assert_ne!( + instruction_result, + InstructionResult::FatalExternalError, + "Fatal external error in insert_eofcreate_outcome" + ); + + let this_gas = interpreter.control.gas(); + if instruction_result.is_ok_or_revert() { + this_gas.erase_cost(outcome.gas().remaining()); + } + + let stack_item = if instruction_result.is_ok() { + this_gas.record_refund(outcome.gas().refunded()); + outcome.address.unwrap_or_default().into_word().into() + } else { + U256::ZERO + }; + + // Safe to push without stack limit check + let _ = interpreter.stack.push(stack_item); + } + FrameResult::EOFCreate(outcome) => { + let instruction_result = *outcome.instruction_result(); + let interpreter = &mut self.interpreter; + if instruction_result == InstructionResult::Revert { + // Save data to return data buffer if the create reverted + *interpreter.return_data.buffer_mut() = outcome.output().to_owned() + } else { + // Otherwise clear it. Note that RETURN opcode should abort. + interpreter.return_data.buffer_mut().clear(); + }; + + assert_ne!( + instruction_result, + InstructionResult::FatalExternalError, + "Fatal external error in insert_eofcreate_outcome" + ); + + let this_gas = interpreter.control.gas(); + if instruction_result.is_ok_or_revert() { + this_gas.erase_cost(outcome.gas().remaining()); + } + + let stack_item = if instruction_result.is_ok() { + this_gas.record_refund(outcome.gas().refunded()); + outcome.address.expect("EOF Address").into_word().into() + } else { + U256::ZERO + }; + + // Safe to push without stack limit check + let _ = interpreter.stack.push(stack_item); + } + } + + Ok(()) + } +} + +pub fn return_create( + journal: &mut Journal, + checkpoint: JournalCheckpoint, + interpreter_result: &mut InterpreterResult, + address: Address, + max_code_size: usize, + spec_id: SpecId, +) { + // if return is not ok revert and return. + if !interpreter_result.result.is_ok() { + journal.checkpoint_revert(checkpoint); + return; + } + // Host error if present on execution + // if ok, check contract creation limit and calculate gas deduction on output len. + // + // EIP-3541: Reject new contract code starting with the 0xEF byte + if spec_id.is_enabled_in(LONDON) && interpreter_result.output.first() == Some(&0xEF) { + journal.checkpoint_revert(checkpoint); + interpreter_result.result = InstructionResult::CreateContractStartingWithEF; + return; + } + + // EIP-170: Contract code size limit + // By default limit is 0x6000 (~25kb) + if spec_id.is_enabled_in(SPURIOUS_DRAGON) && interpreter_result.output.len() > max_code_size { + journal.checkpoint_revert(checkpoint); + interpreter_result.result = InstructionResult::CreateContractSizeLimit; + return; + } + let gas_for_code = interpreter_result.output.len() as u64 * gas::CODEDEPOSIT; + if !interpreter_result.gas.record_cost(gas_for_code) { + // record code deposit gas cost and check if we are out of gas. + // EIP-2 point 3: If contract creation does not have enough gas to pay for the + // final gas fee for adding the contract code to the state, the contract + // creation fails (i.e. goes out-of-gas) rather than leaving an empty contract. + if spec_id.is_enabled_in(HOMESTEAD) { + journal.checkpoint_revert(checkpoint); + interpreter_result.result = InstructionResult::OutOfGas; + return; + } else { + interpreter_result.output = Bytes::new(); + } + } + // if we have enough gas we can commit changes. + journal.checkpoint_commit(); + + // Do analysis of bytecode straight away. + let bytecode = Bytecode::new_legacy(interpreter_result.output.clone()); + + // set code + journal.set_code(address, bytecode); + + interpreter_result.result = InstructionResult::Return; +} + +pub fn return_eofcreate( + journal: &mut Journal, + checkpoint: JournalCheckpoint, + interpreter_result: &mut InterpreterResult, + address: Address, + max_code_size: usize, +) { + // Note we still execute RETURN opcode and return the bytes. + // In EOF those opcodes should abort execution. + // + // In RETURN gas is still protecting us from ddos and in oog, + // behaviour will be same as if it failed on return. + // + // Bytes of RETURN will drained in `insert_eofcreate_outcome`. + if interpreter_result.result != InstructionResult::ReturnContract { + journal.checkpoint_revert(checkpoint); + return; + } + + if interpreter_result.output.len() > max_code_size { + journal.checkpoint_revert(checkpoint); + interpreter_result.result = InstructionResult::CreateContractSizeLimit; + return; + } + + // deduct gas for code deployment. + let gas_for_code = interpreter_result.output.len() as u64 * gas::CODEDEPOSIT; + if !interpreter_result.gas.record_cost(gas_for_code) { + journal.checkpoint_revert(checkpoint); + interpreter_result.result = InstructionResult::OutOfGas; + return; + } + + journal.checkpoint_commit(); + + // decode bytecode has a performance hit, but it has reasonable restrains. + let bytecode = Eof::decode(interpreter_result.output.clone()).expect("Eof is already verified"); + + // eof bytecode is going to be hashed. + journal.set_code(address, Bytecode::Eof(Arc::new(bytecode))); +} + +pub trait EthFrameContext: + TransactionGetter + Host + ErrorGetter + BlockGetter + JournalStateGetter + CfgGetter +{ +} + +impl< + ERROR, + CTX: TransactionGetter + + ErrorGetter + + BlockGetter + + JournalStateGetter + + CfgGetter + + Host, + > EthFrameContext for CTX +{ +} + +pub trait EthFrameError: + From> + From +{ +} + +impl> + From> + EthFrameError for T +{ +} ``` diff --git a/contract-derive/Cargo.toml b/contract-derive/Cargo.toml index bf8a4ed..673840b 100644 --- a/contract-derive/Cargo.toml +++ b/contract-derive/Cargo.toml @@ -8,6 +8,7 @@ proc-macro2 = "1.0" quote = "1.0" syn = { version = "1.0", features = ["full"] } alloy-core = { version = "0.7.4", default-features = false } +alloy-sol-types = { version = "0.7.4", default-features = false } [lib] proc-macro = true diff --git a/contract-derive/src/lib.rs b/contract-derive/src/lib.rs index 233f276..d312038 100644 --- a/contract-derive/src/lib.rs +++ b/contract-derive/src/lib.rs @@ -1,15 +1,16 @@ extern crate proc_macro; use alloy_core::primitives::keccak256; +use alloy_sol_types::SolValue; use proc_macro::TokenStream; use quote::{format_ident, quote}; -use syn::{parse_macro_input, ImplItem, ItemImpl, DeriveInput, Data, Fields}; +use syn::{parse_macro_input, Data, DeriveInput, Fields, ImplItem, ItemImpl, ItemTrait, TraitItem}; use syn::{FnArg, ReturnType}; #[proc_macro_derive(Event, attributes(indexed))] pub fn event_derive(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let name = &input.ident; - + let fields = if let Data::Struct(data) = &input.data { if let Fields::Named(fields) = &data.fields { &fields.named @@ -25,9 +26,7 @@ pub fn event_derive(input: TokenStream) -> TokenStream { let field_types: Vec<_> = fields.iter().map(|f| &f.ty).collect(); let indexed_fields: Vec<_> = fields .iter() - .filter(|f| { - f.attrs.iter().any(|attr| attr.path.is_ident("indexed")) - }) + .filter(|f| f.attrs.iter().any(|attr| attr.path.is_ident("indexed"))) .map(|f| &f.ident) .collect(); @@ -62,7 +61,7 @@ pub fn event_derive(input: TokenStream) -> TokenStream { #( if !first { signature.extend_from_slice(b","); } first = false; - + signature.extend_from_slice(self.#field_names.sol_type_name().as_bytes()); let encoded = self.#field_names.abi_encode(); @@ -174,29 +173,29 @@ pub fn contract(_attr: TokenStream, item: TokenStream) -> TokenStream { $arg.sol_type_name().as_bytes() }; } - + #[macro_export] macro_rules! emit { ($event:ident, $($field:expr),*) => {{ use alloy_sol_types::SolValue; use alloy_core::primitives::{keccak256, B256, U256, I256}; use alloc::vec::Vec; - + let mut signature = alloc::vec![]; signature.extend_from_slice($event::NAME.as_bytes()); signature.extend_from_slice(b"("); - + let mut first = true; let mut topics = alloc::vec![B256::default()]; let mut data = Vec::new(); - + $( if !first { signature.extend_from_slice(b","); } first = false; - + signature.extend_from_slice(get_type_signature!($field)); let encoded = $field.abi_encode(); - + let field_ident = stringify!($field); if $event::INDEXED_FIELDS.contains(&field_ident) && topics.len() < 4 { topics.push(B256::from_slice(&encoded)); @@ -204,10 +203,10 @@ pub fn contract(_attr: TokenStream, item: TokenStream) -> TokenStream { data.extend_from_slice(&encoded); } )* - + signature.extend_from_slice(b")"); topics[0] = B256::from(keccak256(&signature)); - + if !data.is_empty() { eth_riscv_runtime::emit_log(&data, &topics); } else if topics.len() > 1 { @@ -275,4 +274,140 @@ fn is_payable(method: &syn::ImplItemMethod) -> bool { } false }) -} \ No newline at end of file +} + +#[proc_macro_attribute] +pub fn interface(_attr: TokenStream, item: TokenStream) -> TokenStream { + // DEBUG + println!("\n=== Input Trait ==="); + println!("{}", item.to_string()); + + let input = parse_macro_input!(item as ItemTrait); + let trait_name = &input.ident; + + let method_impls: Vec<_> = input + .items + .iter() + .map(|item| { + if let TraitItem::Method(method) = item { + let method_name = &method.sig.ident; + let selector_bytes = keccak256(method_name.to_string())[..4] + .try_into() + .unwrap_or_default(); + let method_selector = u32::from_be_bytes(selector_bytes); + + // DEBUG + println!("\n=== Processing Method ==="); + println!("Method name: {}", method_name); + println!("Selector bytes: {:02x?}", selector_bytes); + println!("Selector u32: {}", method_selector); + + // Extract argument types and names, skipping self + let arg_types: Vec<_> = method + .sig + .inputs + .iter() + .skip(1) + .map(|arg| { + if let FnArg::Typed(pat_type) = arg { + let ty = &*pat_type.ty; + quote! { #ty } + } else { + panic!("Expected typed arguments"); + } + }) + .collect(); + let arg_names: Vec<_> = (0..method.sig.inputs.len() - 1) + .map(|i| format_ident!("arg{}", i)) + .collect(); + + // Get the return type + let return_type = match &method.sig.output { + ReturnType::Default => quote! { () }, + ReturnType::Type(_, ty) => + quote! { #ty }, + }; + + // Generate calldata with different encoding depending on # of args + let args_encoding = if arg_names.is_empty() { + quote! { + let mut complete_calldata = Vec::with_capacity(4); + complete_calldata.extend_from_slice(&[ + #method_selector.to_be_bytes()[0], + #method_selector.to_be_bytes()[1], + #method_selector.to_be_bytes()[2], + #method_selector.to_be_bytes()[3], + ]); + } + } else if arg_names.len() == 1 { + quote! { + let mut args_calldata = #(#arg_names),*.abi_encode(); + let mut complete_calldata = Vec::with_capacity(4 + args_calldata.len()); + complete_calldata.extend_from_slice(&[ + #method_selector.to_be_bytes()[0], + #method_selector.to_be_bytes()[1], + #method_selector.to_be_bytes()[2], + #method_selector.to_be_bytes()[3], + ]); + complete_calldata.append(&mut args_calldata); + } + } else { + quote! { + let mut args_calldata = (#(#arg_names),*).abi_encode(); + let mut complete_calldata = Vec::with_capacity(4 + args_calldata.len()); + complete_calldata.extend_from_slice(&[ + #method_selector.to_be_bytes()[0], + #method_selector.to_be_bytes()[1], + #method_selector.to_be_bytes()[2], + #method_selector.to_be_bytes()[3], + ]); + complete_calldata.append(&mut args_calldata); + } + }; + + Some(quote! { + pub fn #method_name(&self, #(#arg_names: #arg_types),*) -> Option<#return_type> { + use alloy_sol_types::SolValue; + use alloc::vec::Vec; + + #args_encoding + + // Make the call + let result = eth_riscv_runtime::call_contract( + self.address, + 0_u64, + &complete_calldata, + 32_u64 // TODO: Figure out how to use SolType to get the return size + + )?; + + // Decode result + <#return_type>::abi_decode(&result, true).ok() + } + }) + } else { + panic!("Expected methods arguments"); + } + }) + .collect(); + + let expanded = quote! { + pub struct #trait_name { + address: Address, + } + + impl #trait_name { + pub fn new(address: Address) -> Self { + Self { address } + } + + #(#method_impls)* + } + }; + + // DEBUG + println!("\n=== Generated Code ==="); + println!("{:#}", expanded.to_string().replace(';', ";\n")); + + TokenStream::from(expanded) +} diff --git a/erc20/src/lib.rs b/erc20/src/lib.rs index 75ba1c4..7ce56ed 100644 --- a/erc20/src/lib.rs +++ b/erc20/src/lib.rs @@ -3,13 +3,13 @@ use core::default::Default; -use contract_derive::{contract, payable, Event}; +use contract_derive::{contract, interface, payable, Event}; use eth_riscv_runtime::types::Mapping; use alloy_core::primitives::{address, Address, U256}; extern crate alloc; -use alloc::string::String; +use alloc::{string::String, vec::Vec}; #[derive(Default)] pub struct ERC20 { @@ -20,6 +20,17 @@ pub struct ERC20 { symbol: String, decimals: u8, } +#[derive(Event)] +pub struct DebugCalldata { + #[indexed] + pub target: Address, + pub calldata: Vec, +} + +#[interface] +trait IERC20 { + fn balance_of(&self, owner: Address) -> u64; +} #[derive(Event)] pub struct Transfer { @@ -41,6 +52,14 @@ pub struct Mint { #[contract] impl ERC20 { + pub fn x_balance_of(&self, owner: Address, target: Address) -> u64 { + let token = IERC20::new(target); + match token.balance_of(owner) { + Some(balance) => balance, + _ => eth_riscv_runtime::revert(), + } + } + pub fn balance_of(&self, owner: Address) -> u64 { self.balances.read(owner) } diff --git a/eth-riscv-runtime/src/call.rs b/eth-riscv-runtime/src/call.rs new file mode 100644 index 0000000..c832db5 --- /dev/null +++ b/eth-riscv-runtime/src/call.rs @@ -0,0 +1,21 @@ +#![no_std] + +extern crate alloc; +use alloc::vec::Vec; +use alloy_core::primitives::{Address, Bytes, B256}; + +pub fn call_contract(addr: Address, value: u64, data: &[u8], ret_size: u64) -> Option { + let mut ret_data = Vec::with_capacity(ret_size as usize); + ret_data.resize(ret_size as usize, 0); + + crate::call( + addr, + value, + data.as_ptr() as u64, + data.len() as u64, + ret_data.as_ptr() as u64, + ret_size, + ); + + Some(Bytes::from(ret_data)) +} diff --git a/eth-riscv-runtime/src/lib.rs b/eth-riscv-runtime/src/lib.rs index 317a6a3..41fb40d 100644 --- a/eth-riscv-runtime/src/lib.rs +++ b/eth-riscv-runtime/src/lib.rs @@ -2,20 +2,23 @@ #![no_main] #![feature(alloc_error_handler, maybe_uninit_write_slice, round_char_boundary)] -use alloy_core::primitives::{Address, B256, U256}; +use alloy_core::primitives::{Address, Bytes, B256, U256}; use core::arch::asm; use core::panic::PanicInfo; use core::slice; pub use riscv_rt::entry; mod alloc; -pub mod types; pub mod block; pub mod tx; +pub mod types; pub mod log; pub use log::{emit_log, Event}; +pub mod call; +pub use call::call_contract; + const CALLDATA_ADDRESS: usize = 0x8000_0000; pub trait Contract { @@ -66,14 +69,34 @@ pub fn sload(key: u64) -> U256 { pub fn sstore(key: u64, value: U256) { let limbs = value.as_limbs(); - unsafe { + unsafe { asm!("ecall", in("a0") key, in("a1") limbs[0], in("a2") limbs[1], in("a3") limbs[2], in("a4") limbs[3], in("t0") u8::from(Syscall::SStore)); } } -pub fn call(addr: u64, value: u64, in_mem: u64, in_size: u64, out_mem: u64, out_size: u64) { +pub fn call( + addr: Address, + value: u64, + data_offset: u64, + data_size: u64, + res_offset: u64, + res_size: u64, +) { + let addr: U256 = addr.into_word().into(); + let addr = addr.as_limbs(); unsafe { - asm!("ecall", in("a0") addr, in("a1") value, in("a2") in_mem, in("a3") in_size, in("a4") out_mem, in("a5") out_size, in("t0") u8::from(Syscall::Call)); + asm!( + "ecall", + in("a0") addr[0], + in("a1") addr[1], + in("a2") addr[2], + in("a3") value, + in("a4") data_offset, + in("a5") data_size, + in("a6") res_offset, + in("a7") res_size, + in("t0") u8::from(Syscall::Call) + ); } } @@ -145,7 +168,9 @@ pub fn msg_sig() -> [u8; 4] { pub fn msg_data() -> &'static [u8] { let length = unsafe { slice_from_raw_parts(CALLDATA_ADDRESS, 8) }; - let length = u64::from_le_bytes([length[0], length[1], length[2], length[3], length[4], length[5], length[6], length[7]]) as usize; + let length = u64::from_le_bytes([ + length[0], length[1], length[2], length[3], length[4], length[5], length[6], length[7], + ]) as usize; unsafe { slice_from_raw_parts(CALLDATA_ADDRESS + 8, length) } } diff --git a/eth-riscv-runtime/src/types.rs b/eth-riscv-runtime/src/types/mapping.rs similarity index 100% rename from eth-riscv-runtime/src/types.rs rename to eth-riscv-runtime/src/types/mapping.rs diff --git a/eth-riscv-runtime/src/types/mod.rs b/eth-riscv-runtime/src/types/mod.rs new file mode 100644 index 0000000..3034c0e --- /dev/null +++ b/eth-riscv-runtime/src/types/mod.rs @@ -0,0 +1,3 @@ +mod mapping; + +pub use mapping::{Mapping, StorageStorable}; diff --git a/r55/rvemu.dtb b/r55/rvemu.dtb new file mode 100644 index 0000000000000000000000000000000000000000..47c9f54671a68d8e9b2f850ba5abb28a3ae680bc GIT binary patch literal 1598 zcmah}J#Q2-5cL8H1c(TNz5=;-OFQMBiEl2vxs8{2#0s8Uc+ra}~S z)U^BsT7Cc>HT(p`d-nQn6Ob5b&NK7o%j4Pmw*LF45UbCG5c@)`JjMAKd>_041|`>E zzY*k1ze%TE#~E?>im2a%9QAd`15;QzOJ{{~g@#U|?*s6oJ~lj4RZA4b<%zbc_A-4R zf`>+Hcd(tS+4d~YHjUnty0*Gh2hPo3juyVGKi+OL<0dH2FH86 z=;Wxs68jUFJkA+aRJvh7@?)Xz8hB9KoxSmRltCu>+F=5RRaI!;8M_Pcq9%J_qFA%v zL$7A6>QDOK1rI|0A@f5GHD%LUDxjE?a=!-WMT+>D`0)N5dV<)r${J#cEL-c88BqZx zQXADNG43txVBR${b4;%Iz3cp9+y#7KTGaP*?erC#AN6TArvr#FtKhIxv;-UX?30=8 z?G{ybluO&Kk@vp??6cpqp`Gnw+d)ZD<2I&kT#6-fMwg{B9$EJBg??bxBz*-_=Db4v zAt!`P5|(8g?-%bM&!ke7HuK4TZy}lMCio&?kV1|PoPGx9w194fgSir`eKE%V@7v?O z2ezo1Ql-;w3m8ZG4*us1CbIX%T)q=yW8EK+$DO|DmrF?M8lx|xlXJ(&&k=95C)td|PJrjlf3N z$ZK0!o*QLtDpgrpv1N-ls*|Ozn#>i`Qi)3EsuWZFXJzKt3hQ#+x)kV%SS+7iia?C2 zosgxLr|FSVXRWH`To`pMTv4fMW>wG3Z>BlP>Kv8uk61``=;n<=nats>!zc@ziK=Q- gk8CZfcD?rW; + #size-cells = <0x02>; + compatible = "riscv-virtio"; + model = "riscv-virtio,qemu"; + + chosen { + bootargs = "root=/dev/vda ro console=ttyS0"; + stdout-path = "/uart@10000000"; + }; + + uart@10000000 { + interrupts = <0xa>; + interrupt-parent = <0x03>; + clock-frequency = <0x384000>; + reg = <0x0 0x10000000 0x0 0x100>; + compatible = "ns16550a"; + }; + + virtio_mmio@10001000 { + interrupts = <0x01>; + interrupt-parent = <0x03>; + reg = <0x0 0x10001000 0x0 0x1000>; + compatible = "virtio,mmio"; + }; + + cpus { + #address-cells = <0x01>; + #size-cells = <0x00>; + timebase-frequency = <0x989680>; + + cpu-map { + cluster0 { + core0 { + cpu = <0x01>; + }; + }; + }; + + cpu@0 { + phandle = <0x01>; + device_type = "cpu"; + reg = <0x00>; + status = "okay"; + compatible = "riscv"; + riscv,isa = "rv64imafdcsu"; + mmu-type = "riscv,sv48"; + + interrupt-controller { + #interrupt-cells = <0x01>; + interrupt-controller; + compatible = "riscv,cpu-intc"; + phandle = <0x02>; + }; + }; + }; + + memory@80000000 { + device_type = "memory"; + reg = <0x0 0x80000000 0x0 0x8000000>; + }; + + soc { + #address-cells = <0x02>; + #size-cells = <0x02>; + compatible = "simple-bus"; + ranges; + + interrupt-controller@c000000 { + phandle = <0x03>; + riscv,ndev = <0x35>; + reg = <0x00 0xc000000 0x00 0x4000000>; + interrupts-extended = <0x02 0x0b 0x02 0x09>; + interrupt-controller; + compatible = "riscv,plic0"; + #interrupt-cells = <0x01>; + #address-cells = <0x00>; + }; + + clint@2000000 { + interrupts-extended = <0x02 0x03 0x02 0x07>; + reg = <0x00 0x2000000 0x00 0x10000>; + compatible = "riscv,clint0"; + }; + }; +}; \ No newline at end of file diff --git a/r55/src/exec.rs b/r55/src/exec.rs index 181a3af..d8efe57 100644 --- a/r55/src/exec.rs +++ b/r55/src/exec.rs @@ -5,8 +5,8 @@ use eth_riscv_syscalls::Syscall; use revm::{ handler::register::EvmHandler, interpreter::{ - CallInputs, CallScheme, CallValue, Host, InstructionResult, Interpreter, InterpreterAction, - InterpreterResult, SharedMemory, + CallInputs, CallScheme, CallValue, Gas, Host, InstructionResult, Interpreter, + InterpreterAction, InterpreterResult, SharedMemory, }, primitives::{address, Address, Bytes, ExecutionResult, Log, Output, TransactTo, B256, U256}, Database, Evm, Frame, FrameOrResult, InMemoryDB, @@ -15,12 +15,14 @@ use rvemu::{emulator::Emulator, exception::Exception}; use std::{collections::BTreeMap, rc::Rc, sync::Arc}; use super::error::{Error, Result, TxResult}; +use super::gas; +use super::syscall_gas; pub fn deploy_contract(db: &mut InMemoryDB, bytecode: Bytes) -> Result
{ let mut evm = Evm::builder() .with_db(db) .modify_tx_env(|tx| { - tx.caller = address!("0000000000000000000000000000000000000001"); + tx.caller = address!("000000000000000000000000000000000000000A"); tx.transact_to = TransactTo::Create; tx.data = bytecode; tx.value = U256::from(0); @@ -47,7 +49,7 @@ pub fn run_tx(db: &mut InMemoryDB, addr: &Address, calldata: Vec) -> Result< let mut evm = Evm::builder() .with_db(db) .modify_tx_env(|tx| { - tx.caller = address!("0000000000000000000000000000000000000001"); + tx.caller = address!("000000000000000000000000000000000000000A"); tx.transact_to = TransactTo::Call(*addr); tx.data = calldata.into(); tx.value = U256::from(0); @@ -89,15 +91,26 @@ struct RVEmu { fn riscv_context(frame: &Frame) -> Option { let interpreter = frame.interpreter(); + println!("Creating RISC-V context:"); + println!("Contract address: {}", interpreter.contract.target_address); + println!("Input size: {}", interpreter.contract.input.len()); + let Some((0xFF, bytecode)) = interpreter.bytecode.split_first() else { return None; }; + println!("RISC-V bytecode size: {}", bytecode.len()); match setup_from_elf(bytecode, &interpreter.contract.input) { - Ok(emu) => Some(RVEmu { - emu, - returned_data_destiny: None, - }), + Ok(emu) => { + println!( + "RISC-V emulator setup successfully with entry point: 0x{:x}", + emu.cpu.pc + ); + Some(RVEmu { + emu, + returned_data_destiny: None, + }) + } Err(err) => { println!("Failed to setup from ELF: {err}"); None @@ -114,7 +127,16 @@ pub fn handle_register(handler: &mut EvmHandler<'_, EXT, DB>) handler.execution.call = Arc::new(move |ctx, inputs| { let result = old_handle(ctx, inputs); if let Ok(FrameOrResult::Frame(frame)) = &result { - call_stack_inner.borrow_mut().push(riscv_context(frame)); + println!("----"); + println!("Frame created successfully"); + println!("Contract: {}", frame.interpreter().contract.target_address); + println!("Code size: {}", frame.interpreter().bytecode.len()); + + let context = riscv_context(frame); + println!("RISC-V context created: {}", context.is_some()); + println!("----"); + + call_stack_inner.borrow_mut().push(context); } result }); @@ -133,16 +155,57 @@ pub fn handle_register(handler: &mut EvmHandler<'_, EXT, DB>) // execute riscv context or old logic. let old_handle = handler.execution.execute_frame.clone(); handler.execution.execute_frame = Arc::new(move |frame, memory, instraction_table, ctx| { - let result = if let Some(Some(riscv_context)) = call_stack.borrow_mut().first_mut() { + let depth = call_stack.borrow().len() - 1; + let mut result = if let Some(Some(riscv_context)) = call_stack.borrow_mut().last_mut() { + println!( + "\n*[{}] RISC-V Emu Handler (with PC: 0x{:x})", + depth, riscv_context.emu.cpu.pc + ); + execute_riscv(riscv_context, frame.interpreter_mut(), memory, ctx)? } else { + println!("\n*[OLD Handler]"); old_handle(frame, memory, instraction_table, ctx)? }; // if it is return pop the stack. if result.is_return() { + println!("=== RETURN Frame ==="); call_stack.borrow_mut().pop(); + println!( + "Popped frame from stack. Remaining frames: {}", + call_stack.borrow().len() + ); + + // if cross-contract call, copy return data into memory range expected by the parent + if !call_stack.borrow().is_empty() { + if let Some(Some(parent)) = call_stack.borrow_mut().last_mut() { + if let Some(return_range) = &parent.returned_data_destiny { + if let InterpreterAction::Return { result: res } = &mut result { + // Get allocated memory slice + let return_memory = parent + .emu + .cpu + .bus + .get_dram_slice(return_range.clone()) + .expect("unable to get memory from return range"); + + println!("Return data: {:?}", res.output); + println!("Memory range: {:?}", return_range); + println!("Memory size: {}", return_memory.len()); + + // Write return data to parent's memory + if res.output.len() == return_memory.len() { + println!("Copying output to memory"); + return_memory.copy_from_slice(&res.output); + } + } + } + } + } } + println!("Frame({}) Gas: {:#?}", depth, frame.interpreter().gas); + Ok(result) }); } @@ -153,12 +216,28 @@ fn execute_riscv( shared_memory: &mut SharedMemory, host: &mut dyn Host, ) -> Result { + println!( + "{} RISC-V execution:\n PC: {:#x}\n Contract: {}\n Return data dst: {:#?}", + if rvemu.emu.cpu.pc == 0x80300000 { + "Starting" + } else { + "Resuming" + }, + rvemu.emu.cpu.pc, + interpreter.contract.target_address, + &rvemu.returned_data_destiny + ); + let emu = &mut rvemu.emu; emu.cpu.is_count = true; + let returned_data_destiny = &mut rvemu.returned_data_destiny; if let Some(destiny) = std::mem::take(returned_data_destiny) { let data = emu.cpu.bus.get_dram_slice(destiny)?; - data.copy_from_slice(shared_memory.slice(0, data.len())) + if shared_memory.len() >= data.len() { + data.copy_from_slice(shared_memory.slice(0, data.len())); + } + println!("Loaded return data: {}", Bytes::copy_from_slice(data)); } let return_revert = |interpreter: &mut Interpreter, gas_used: u64| { @@ -172,8 +251,7 @@ fn execute_riscv( }, }) }; - // Tracks gas usage across EVM host calls - let mut evm_gas = 0; + // Run emulator and capture ecalls loop { let run_result = emu.start(); @@ -183,8 +261,9 @@ fn execute_riscv( let Ok(syscall) = Syscall::try_from(t0 as u8) else { println!("Unhandled syscall: {:?}", t0); - return return_revert(interpreter, evm_gas); + return return_revert(interpreter, interpreter.gas.spent()); }; + println!("> [Syscall::{} - {:#04x}]", syscall, t0); match syscall { Syscall::Return => { @@ -192,24 +271,14 @@ fn execute_riscv( let ret_size: u64 = emu.cpu.xregs.read(11); let r55_gas = r55_gas_used(&emu.cpu.inst_counter); - let data_bytes = dram_slice(emu, ret_offset, ret_size)?; + println!("> total R55 gas: {}", r55_gas); - let total_cost = r55_gas + evm_gas; - println!( - "evm gas: {}, r55 gas: {}, total cost: {}", - evm_gas, r55_gas, total_cost - ); - let in_limit = interpreter.gas.record_cost(total_cost); - if !in_limit { - return Ok(InterpreterAction::Return { - result: InterpreterResult { - result: InstructionResult::OutOfGas, - output: Bytes::new(), - gas: interpreter.gas, - }, - }); - } + // RETURN logs the gas of the whole risc-v instruction set + syscall_gas!(interpreter, r55_gas); + + let data_bytes = dram_slice(emu, ret_offset, ret_size)?; + println!("interpreter remaining gas: {}", interpreter.gas.remaining()); return Ok(InterpreterAction::Return { result: InterpreterResult { result: InstructionResult::Return, @@ -220,21 +289,32 @@ fn execute_riscv( } Syscall::SLoad => { let key: u64 = emu.cpu.xregs.read(10); + println!( + "SLOAD ({}) - Key: {}", + interpreter.contract.target_address, key + ); match host.sload(interpreter.contract.target_address, U256::from(key)) { Some((value, is_cold)) => { + println!( + "SLOAD ({}) - Value: {}", + interpreter.contract.target_address, value + ); let limbs = value.as_limbs(); emu.cpu.xregs.write(10, limbs[0]); emu.cpu.xregs.write(11, limbs[1]); emu.cpu.xregs.write(12, limbs[2]); emu.cpu.xregs.write(13, limbs[3]); - if is_cold { - evm_gas += 2100 - } else { - evm_gas += 100 - } + syscall_gas!( + interpreter, + if is_cold { + gas::SLOAD_COLD + } else { + gas::SLOAD_WARM + } + ); } _ => { - return return_revert(interpreter, evm_gas); + return return_revert(interpreter, interpreter.gas.spent()); } } } @@ -250,45 +330,89 @@ fn execute_riscv( U256::from_limbs([first, second, third, fourth]), ); if let Some(result) = result { - if result.is_cold { - evm_gas += 2200 - } else { - evm_gas += 100 - } + syscall_gas!( + interpreter, + if result.is_cold { + gas::SSTORE_COLD + } else { + gas::SSTORE_WARM + } + ); } } Syscall::Call => { let a0: u64 = emu.cpu.xregs.read(10); - let address = - Address::from_slice(emu.cpu.bus.get_dram_slice(a0..(a0 + 20))?); - let value: u64 = emu.cpu.xregs.read(11); - let args_offset: u64 = emu.cpu.xregs.read(12); - let args_size: u64 = emu.cpu.xregs.read(13); - let ret_offset = emu.cpu.xregs.read(14); - let ret_size = emu.cpu.xregs.read(15); - - *returned_data_destiny = Some(ret_offset..(ret_offset + ret_size)); + let a1: u64 = emu.cpu.xregs.read(11); + let a2: u64 = emu.cpu.xregs.read(12); + let addr = Address::from_word(U256::from_limbs([a0, a1, a2, 0]).into()); + let value: u64 = emu.cpu.xregs.read(13); + + // Get calldata + let args_offset: u64 = emu.cpu.xregs.read(14); + let args_size: u64 = emu.cpu.xregs.read(15); + let calldata: Bytes = emu + .cpu + .bus + .get_dram_slice(args_offset..(args_offset + args_size)) + .unwrap_or(&mut []) + .to_vec() + .into(); + + // Store where return data should go + let ret_offset = emu.cpu.xregs.read(16); + let ret_size = emu.cpu.xregs.read(17); + println!( + "Return data will be written to: {}..{}", + ret_offset, + ret_offset + ret_size + ); - let tx = &host.env().tx; + // Initialize memory region for return data + let return_memory = emu + .cpu + .bus + .get_dram_slice(ret_offset..(ret_offset + ret_size))?; + return_memory.fill(0); + rvemu.returned_data_destiny = Some(ret_offset..(ret_offset + ret_size)); + + // Calculate gas cost of the call + // TODO: check correctness (tried using evm.codes as ref but i'm no gas wizard) + // TODO: unsure whether memory expansion cost is missing (should be captured in the risc-v costs) + let (empty_account_cost, addr_access_cost) = match host.load_account(addr) { + Some(account) => { + if account.is_cold { + (0, gas::CALL_NEW_ACCOUNT) + } else { + (0, gas::CALL_BASE) + } + } + None => (gas::CALL_EMPTY_ACCOUNT, gas::CALL_NEW_ACCOUNT), + }; + let value_cost = if value != 0 { gas::CALL_VALUE } else { 0 }; + let call_gas_cost = empty_account_cost + addr_access_cost + value_cost; + syscall_gas!(interpreter, call_gas_cost); + + // proactively spend gas limit as the remaining will be refunded (otherwise it underflows) + let call_gas_limit = interpreter.gas.remaining(); + syscall_gas!(interpreter, call_gas_limit); + + println!("Call context:"); + println!(" Caller: {}", interpreter.contract.target_address); + println!(" Target Address: {}", addr); + println!(" Value: {}", value); + println!(" Calldata: {:?}", calldata); return Ok(InterpreterAction::Call { inputs: Box::new(CallInputs { - input: emu - .cpu - .bus - .get_dram_slice(args_offset..(args_offset + args_size))? - .to_vec() - .into(), - gas_limit: tx.gas_limit, - target_address: address, - bytecode_address: address, + input: calldata, + gas_limit: call_gas_limit, + target_address: addr, + bytecode_address: addr, caller: interpreter.contract.target_address, - value: CallValue::Transfer(U256::from_le_bytes( - value.to_le_bytes(), - )), + value: CallValue::Transfer(U256::from(value)), scheme: CallScheme::Call, is_static: false, is_eof: false, - return_memory_offset: 0..ret_size as usize, + return_memory_offset: 0..0, // handled with `returned_data_destiny` }), }); } @@ -442,9 +566,14 @@ fn execute_riscv( } } } - _ => { - let total_cost = r55_gas_used(&emu.cpu.inst_counter) + evm_gas; - return return_revert(interpreter, total_cost); + Ok(_) => { + println!("Successful instruction at PC: {:#x}", emu.cpu.pc); + continue; + } + Err(e) => { + println!("Execution error: {:#?}", e); + syscall_gas!(interpreter, r55_gas_used(&emu.cpu.inst_counter)); + return return_revert(interpreter, interpreter.gas.spent()); } } } diff --git a/r55/src/gas.rs b/r55/src/gas.rs new file mode 100644 index 0000000..099bab8 --- /dev/null +++ b/r55/src/gas.rs @@ -0,0 +1,42 @@ +// Standard EVM operation costs +pub const SLOAD_COLD: u64 = 2100; +pub const SLOAD_WARM: u64 = 100; +pub const SSTORE_COLD: u64 = 2200; +pub const SSTORE_WARM: u64 = 100; + +// Call-related costs +pub const CALL_EMPTY_ACCOUNT: u64 = 25000; +pub const CALL_NEW_ACCOUNT: u64 = 2600; +pub const CALL_VALUE: u64 = 9000; +pub const CALL_BASE: u64 = 100; + +// Macro to handle gas accounting for syscalls. +// Returns OutOfGas InterpreterResult if gas limit is exceeded. +#[macro_export] +macro_rules! syscall_gas { + ($interpreter:expr, $gas_cost:expr $(,)?) => {{ + let remaining_before = $interpreter.gas.remaining(); + let gas_cost = $gas_cost; + + println!("> About to log gas costs:"); + println!(" - Operation cost: {}", gas_cost); + println!(" - Gas remaining: {}", remaining_before); + println!(" - Gas limit: {}", $interpreter.gas.limit()); + println!(" - Gas spent: {}", $interpreter.gas.spent()); + + if !$interpreter.gas.record_cost(gas_cost) { + eprintln!("OUT OF GAS"); + return Ok(InterpreterAction::Return { + result: InterpreterResult { + result: InstructionResult::OutOfGas, + output: Bytes::new(), + gas: $interpreter.gas, + }, + }); + } + + println!("> Gas recorded successfully:"); + println!(" - Gas remaining: {}", remaining_before); + println!(" - Gas spent: {}", $interpreter.gas.spent()); + }}; +} diff --git a/r55/src/lib.rs b/r55/src/lib.rs index 1cbd6b1..26c2cda 100644 --- a/r55/src/lib.rs +++ b/r55/src/lib.rs @@ -1,6 +1,7 @@ pub mod exec; mod error; +mod gas; pub mod test_utils; @@ -134,9 +135,9 @@ mod tests { let selector_transfer = get_selector_from_sig("transfer"); let selector_approve = get_selector_from_sig("approve"); let selector_allowance = get_selector_from_sig("allowance"); - let alice: Address = address!("0000000000000000000000000000000000000001"); - let bob: Address = address!("0000000000000000000000000000000000000002"); - let carol: Address = address!("0000000000000000000000000000000000000003"); + let alice: Address = address!("000000000000000000000000000000000000000A"); + let bob: Address = address!("000000000000000000000000000000000000000B"); + let carol: Address = address!("000000000000000000000000000000000000000C"); let value_mint: u64 = 42; let value_transfer: u64 = 21; let mut calldata_alice_balance = alice.abi_encode(); @@ -218,8 +219,8 @@ mod tests { add_contract_to_db(&mut db, CONTRACT_ADDR, bytecode); // Setup addresses - let alice: Address = address!("0000000000000000000000000000000000000001"); - let bob: Address = address!("0000000000000000000000000000000000000002"); + let alice: Address = address!("000000000000000000000000000000000000000A"); + let bob: Address = address!("000000000000000000000000000000000000000B"); // Add balance to Alice's account for gas fees add_balance_to_db(&mut db, alice, 1e18 as u64); diff --git a/r55/tests/e2e.rs b/r55/tests/e2e.rs index 55f8341..1a4bc3b 100644 --- a/r55/tests/e2e.rs +++ b/r55/tests/e2e.rs @@ -1,3 +1,4 @@ +use alloy_primitives::Bytes; use alloy_sol_types::SolValue; use r55::{ compile_deploy, compile_with_prefix, @@ -18,14 +19,17 @@ fn erc20() { let mut db = InMemoryDB::default(); let bytecode = compile_with_prefix(compile_deploy, ERC20_PATH).unwrap(); - let addr = deploy_contract(&mut db, bytecode).unwrap(); + let addr1 = deploy_contract(&mut db, bytecode.clone()).unwrap(); + let addr2 = deploy_contract(&mut db, bytecode).unwrap(); let selector_balance = get_selector_from_sig("balance_of"); + let selector_x_balance = get_selector_from_sig("x_balance_of"); let selector_mint = get_selector_from_sig("mint"); - let alice: Address = address!("0000000000000000000000000000000000000001"); + let alice: Address = address!("000000000000000000000000000000000000000A"); let value_mint: u64 = 42; let mut calldata_balance = alice.abi_encode(); let mut calldata_mint = (alice, value_mint).abi_encode(); + let mut calldata_x_balance = (alice, addr1).abi_encode(); add_balance_to_db(&mut db, alice, 1e18 as u64); @@ -35,6 +39,34 @@ fn erc20() { let mut complete_calldata_mint = selector_mint.to_vec(); complete_calldata_mint.append(&mut calldata_mint); - run_tx(&mut db, &addr, complete_calldata_mint.clone()).unwrap(); - run_tx(&mut db, &addr, complete_calldata_balance.clone()).unwrap(); + let mut complete_calldata_x_balance = selector_x_balance.to_vec(); + complete_calldata_x_balance.append(&mut calldata_x_balance); + + println!("\n----------------------------------------------------------"); + println!("-- MINT TX -----------------------------------------------"); + println!("----------------------------------------------------------"); + println!( + " > TX Calldata: {:#?\n}", + Bytes::from(complete_calldata_mint.clone()) + ); + run_tx(&mut db, &addr1, complete_calldata_mint.clone()).unwrap(); + println!("\n----------------------------------------------------------"); + println!("-- BALANCE OF TX -----------------------------------------"); + println!("----------------------------------------------------------"); + println!( + " > TX Calldata: {:#?}\n", + Bytes::from(complete_calldata_balance.clone()) + ); + run_tx(&mut db, &addr1, complete_calldata_balance.clone()).unwrap(); + println!("\n----------------------------------------------------------"); + println!("-- X-CONTRACT BALANCE OF TX ------------------------------"); + println!("----------------------------------------------------------"); + println!( + " > TX Calldata: {:#?}\n", + Bytes::from(complete_calldata_x_balance.clone()) + ); + match run_tx(&mut db, &addr2, complete_calldata_x_balance.clone()) { + Ok(res) => log::info!("res: {:#?}", res), + Err(e) => log::error!("{:#?}", e), + }; } From f87940ae633a651d42713bc62e0fa5ff51a6e6b2 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Sat, 14 Dec 2024 00:33:21 +0100 Subject: [PATCH 02/11] feat: improve logs --- .env | 1 + Cargo.toml | 5 +- README.md | 149 +++++++++++++++++++++++------------------- r55/Cargo.toml | 7 +- r55/src/error.rs | 15 +++++ r55/src/exec.rs | 115 ++++++++++++++------------------ r55/src/gas.rs | 18 ++--- r55/src/lib.rs | 13 ++-- r55/src/test_utils.rs | 13 +++- r55/tests/e2e.rs | 49 ++++++++------ 10 files changed, 212 insertions(+), 173 deletions(-) create mode 100644 .env diff --git a/.env b/.env new file mode 100644 index 0000000..93edc8b --- /dev/null +++ b/.env @@ -0,0 +1 @@ +RUST_LOG=DEBUG diff --git a/Cargo.toml b/Cargo.toml index 9fff15f..8bd7d8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,8 @@ repository = "https://github.com/leonardoalt/r5" eth-riscv-interpreter = { path = "eth-riscv-interpreter" } eth-riscv-syscalls = { path = "eth-riscv-syscalls" } -env_logger = "0.11.5" eyre = "0.6.12" -log = "0.4.22" thiserror = "2.0.3" + +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/README.md b/README.md index 3c67e67..7edd718 100644 --- a/README.md +++ b/README.md @@ -929,6 +929,8 @@ use rvemu::{emulator::Emulator, exception::Exception}; use std::{collections::BTreeMap, rc::Rc, sync::Arc}; use super::error::{Error, Result, TxResult}; +use super::gas; +use super::syscall_gas; pub fn deploy_contract(db: &mut InMemoryDB, bytecode: Bytes) -> Result
{ let mut evm = Evm::builder() @@ -998,7 +1000,6 @@ pub fn run_tx(db: &mut InMemoryDB, addr: &Address, calldata: Vec) -> Result< struct RVEmu { emu: Emulator, returned_data_destiny: Option>, - evm_gas: u64, } fn riscv_context(frame: &Frame) -> Option { @@ -1022,7 +1023,6 @@ fn riscv_context(frame: &Frame) -> Option { Some(RVEmu { emu, returned_data_destiny: None, - evm_gas: 0, }) } Err(err) => { @@ -1113,9 +1113,6 @@ pub fn handle_register(handler: &mut EvmHandler<'_, EXT, DB>) println!("Copying output to memory"); return_memory.copy_from_slice(&res.output); } - - // Update gas spent - parent.evm_gas += res.gas.spent(); } } } @@ -1144,8 +1141,6 @@ fn execute_riscv( &rvemu.returned_data_destiny ); - println!("interpreter remaining gas: {}", interpreter.gas.remaining()); - let emu = &mut rvemu.emu; emu.cpu.is_count = true; @@ -1179,7 +1174,7 @@ fn execute_riscv( let Ok(syscall) = Syscall::try_from(t0 as u8) else { println!("Unhandled syscall: {:?}", t0); - return return_revert(interpreter, rvemu.evm_gas); + return return_revert(interpreter, interpreter.gas.spent()); }; println!("> [Syscall::{} - {:#04x}]", syscall, t0); @@ -1189,24 +1184,12 @@ fn execute_riscv( let ret_size: u64 = emu.cpu.xregs.read(11); let r55_gas = r55_gas_used(&emu.cpu.inst_counter); - let data_bytes = dram_slice(emu, ret_offset, ret_size)?; + println!("> total R55 gas: {}", r55_gas); - let total_cost = r55_gas + rvemu.evm_gas; - println!( - "evm gas: {}, r55 gas: {}, total cost: {}", - rvemu.evm_gas, r55_gas, total_cost - ); - let in_limit = interpreter.gas.record_cost(total_cost); - if !in_limit { - eprintln!("OUT OF GAS"); - return Ok(InterpreterAction::Return { - result: InterpreterResult { - result: InstructionResult::OutOfGas, - output: Bytes::new(), - gas: interpreter.gas, - }, - }); - } + // RETURN logs the gas of the whole risc-v instruction set + syscall_gas!(interpreter, r55_gas); + + let data_bytes = dram_slice(emu, ret_offset, ret_size)?; println!("interpreter remaining gas: {}", interpreter.gas.remaining()); return Ok(InterpreterAction::Return { @@ -1234,14 +1217,17 @@ fn execute_riscv( emu.cpu.xregs.write(11, limbs[1]); emu.cpu.xregs.write(12, limbs[2]); emu.cpu.xregs.write(13, limbs[3]); - if is_cold { - rvemu.evm_gas += 2100 - } else { - rvemu.evm_gas += 100 - } + syscall_gas!( + interpreter, + if is_cold { + gas::SLOAD_COLD + } else { + gas::SLOAD_WARM + } + ); } _ => { - return return_revert(interpreter, rvemu.evm_gas); + return return_revert(interpreter, interpreter.gas.spent()); } } } @@ -1257,11 +1243,14 @@ fn execute_riscv( U256::from_limbs([first, second, third, fourth]), ); if let Some(result) = result { - if result.is_cold { - rvemu.evm_gas += 2200 - } else { - rvemu.evm_gas += 100 - } + syscall_gas!( + interpreter, + if result.is_cold { + gas::SSTORE_COLD + } else { + gas::SSTORE_WARM + } + ); } } Syscall::Call => { @@ -1300,45 +1289,30 @@ fn execute_riscv( rvemu.returned_data_destiny = Some(ret_offset..(ret_offset + ret_size)); // Calculate gas for the call - // TODO: Check correctness (tried using evm.codes as refjs) + // TODO: Check correctness (tried using evm.codes as ref but i'm no gas wizard) let (empty_account_cost, addr_access_cost) = match host.load_account(addr) { Some(account) => { if account.is_cold { - (0, 2600) + (0, gas::CALL_NEW_ACCOUNT) } else { - (0, 100) + (0, gas::CALL_BASE) } } - None => (25000, 2600), + None => (gas::CALL_EMPTY_ACCOUNT, gas::CALL_NEW_ACCOUNT), }; - let value_cost = if value != 0 { 9000 } else { 0 }; + let value_cost = if value != 0 { gas::CALL_VALUE } else { 0 }; let call_gas_cost = empty_account_cost + addr_access_cost + value_cost; - rvemu.evm_gas += call_gas_cost; - - let r55_gas = r55_gas_used(&emu.cpu.inst_counter); - let total_cost = r55_gas + rvemu.evm_gas; - println!("Gas spent before call: {}", total_cost - call_gas_cost); - println!("Call gas cost {}", call_gas_cost); - // Pass remaining gas to the call - let tx = &host.env().tx; - let remaining_gas = tx.gas_limit.saturating_sub(total_cost); - println!("Remaining gas for call: {}", remaining_gas); - if !interpreter.gas.record_cost(total_cost) { - eprintln!("OUT OF GAS"); - }; - println!("interpreter remaining gas: {}", interpreter.gas.remaining()); + syscall_gas!(interpreter, call_gas_cost); println!("Call context:"); println!(" Caller: {}", interpreter.contract.target_address); println!(" Target Address: {}", addr); println!(" Value: {}", value); - println!(" Call Gas limit: {}", remaining_gas); - println!(" Tx Gas limit: {}", tx.gas_limit); println!(" Calldata: {:?}", calldata); return Ok(InterpreterAction::Call { inputs: Box::new(CallInputs { input: calldata, - gas_limit: remaining_gas, + gas_limit: interpreter.gas.remaining(), target_address: addr, bytecode_address: addr, caller: interpreter.contract.target_address, @@ -1346,7 +1320,7 @@ fn execute_riscv( scheme: CallScheme::Call, is_static: false, is_eof: false, - return_memory_offset: 0..0, // We don't need this anymore + return_memory_offset: 0..0, // handled with `returned_data_destiny` }), }); } @@ -1506,8 +1480,8 @@ fn execute_riscv( } Err(e) => { println!("Execution error: {:#?}", e); - let total_cost = r55_gas_used(&emu.cpu.inst_counter) + rvemu.evm_gas; - return return_revert(interpreter, total_cost); + syscall_gas!(interpreter, r55_gas_used(&emu.cpu.inst_counter)); + return return_revert(interpreter, interpreter.gas.spent()); } } } @@ -1555,6 +1529,52 @@ fn r55_gas_used(inst_count: &BTreeMap) -> u64 { total_cost - abi_decode_cost } + +``` +and here the gas helpers used in exec.rs: +```r55/src/gas.rs +// Standard EVM operation costs +pub const SLOAD_COLD: u64 = 2100; +pub const SLOAD_WARM: u64 = 100; +pub const SSTORE_COLD: u64 = 2200; +pub const SSTORE_WARM: u64 = 100; + +// Call-related costs +pub const CALL_EMPTY_ACCOUNT: u64 = 25000; +pub const CALL_NEW_ACCOUNT: u64 = 2600; +pub const CALL_VALUE: u64 = 9000; +pub const CALL_BASE: u64 = 100; + +// Macro to handle gas accounting for syscalls. +// Returns OutOfGas InterpreterResult if gas limit is exceeded. +#[macro_export] +macro_rules! syscall_gas { + ($interpreter:expr, $gas_cost:expr $(,)?) => {{ + let remaining_before = $interpreter.gas.remaining(); + let gas_cost = $gas_cost; + + println!("> About to log gas costs:"); + println!(" - Operation cost: {}", gas_cost); + println!(" - Gas remaining: {}", remaining_before); + println!(" - Gas limit: {}", $interpreter.gas.limit()); + println!(" - Gas spent: {}", $interpreter.gas.spent()); + + if !$interpreter.gas.record_cost(gas_cost) { + eprintln!("OUT OF GAS"); + return Ok(InterpreterAction::Return { + result: InterpreterResult { + result: InstructionResult::OutOfGas, + output: Bytes::new(), + gas: $interpreter.gas, + }, + }); + } + + println!("> Gas recorded successfully:"); + println!(" - Gas remaining: {}", remaining_before); + println!(" - Gas spent: {}", $interpreter.gas.spent()); + }}; +} ``` as you can see, it is important to understand how revm works first, so here you have some context: @@ -1750,9 +1770,6 @@ it is also worth mentioning that (inside the `handle_register() fn`) i added thi println!("Copying output to memory"); return_memory.copy_from_slice(&res.output); } - - // Update gas spent - parent.evm_gas += res.gas.spent(); } } } @@ -1831,8 +1848,8 @@ fn erc20() { Bytes::from(complete_calldata_x_balance.clone()) ); match run_tx(&mut db, &addr2, complete_calldata_x_balance.clone()) { - Ok(res) => log::info!("res: {:#?}", res), - Err(e) => log::error!("{:#?}", e), + Ok(res) => info!("res: {:#?}", res), + Err(e) => error!("{:#?}", e), }; } ``` diff --git a/r55/Cargo.toml b/r55/Cargo.toml index 67b43ab..e661552 100644 --- a/r55/Cargo.toml +++ b/r55/Cargo.toml @@ -10,13 +10,14 @@ eth-riscv-interpreter.workspace = true eth-riscv-syscalls.workspace = true revm = { version = "9.0.0", features = ["std"] } -rvemu = { git="https://github.com/r55-eth/rvemu.git" } +rvemu = { git = "https://github.com/r55-eth/rvemu.git" } alloy-core = "0.7.4" alloy-primitives = "0.7.4" alloy-sol-types = "0.7.4" -env_logger.workspace = true eyre.workspace = true -log.workspace = true thiserror.workspace = true + +tracing.workspace = true +tracing-subscriber.workspace = true diff --git a/r55/src/error.rs b/r55/src/error.rs index ece9409..bd16b95 100644 --- a/r55/src/error.rs +++ b/r55/src/error.rs @@ -1,5 +1,7 @@ //! R55 crate errors +use core::fmt; + use revm::{ primitives::{EVMError, ExecutionResult, Log}, Database, InMemoryDB, @@ -64,3 +66,16 @@ impl From for EVMError { EVMError::Custom(err.to_string()) } } + +impl fmt::Display for TxResult { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Tx Result:\n> success: {}\n> gas used: {}\n> outcome: {}\n> logs: {:#?}\n", + self.status, + self.gas_used, + revm::primitives::Bytes::from(self.output.clone()), + self.logs, + ) + } +} diff --git a/r55/src/exec.rs b/r55/src/exec.rs index d8efe57..722f748 100644 --- a/r55/src/exec.rs +++ b/r55/src/exec.rs @@ -13,6 +13,7 @@ use revm::{ }; use rvemu::{emulator::Emulator, exception::Exception}; use std::{collections::BTreeMap, rc::Rc, sync::Arc}; +use tracing::{debug, field::debug, warn}; use super::error::{Error, Result, TxResult}; use super::gas; @@ -38,7 +39,7 @@ pub fn deploy_contract(db: &mut InMemoryDB, bytecode: Bytes) -> Result
output: Output::Create(_value, Some(addr)), .. } => { - println!("Deployed at addr: {:?}", addr); + debug!("Deployed at addr: {:?}", addr); Ok(addr) } result => Err(Error::UnexpectedExecResult(result)), @@ -70,7 +71,7 @@ pub fn run_tx(db: &mut InMemoryDB, addr: &Address, calldata: Vec) -> Result< output: Output::Call(value), .. } => { - println!("Tx result: {:?}", value); + debug!("Tx result: {:?}", value); Ok(TxResult { output: value.into(), logs, @@ -91,28 +92,17 @@ struct RVEmu { fn riscv_context(frame: &Frame) -> Option { let interpreter = frame.interpreter(); - println!("Creating RISC-V context:"); - println!("Contract address: {}", interpreter.contract.target_address); - println!("Input size: {}", interpreter.contract.input.len()); - let Some((0xFF, bytecode)) = interpreter.bytecode.split_first() else { return None; }; - println!("RISC-V bytecode size: {}", bytecode.len()); match setup_from_elf(bytecode, &interpreter.contract.input) { - Ok(emu) => { - println!( - "RISC-V emulator setup successfully with entry point: 0x{:x}", - emu.cpu.pc - ); - Some(RVEmu { - emu, - returned_data_destiny: None, - }) - } + Ok(emu) => Some(RVEmu { + emu, + returned_data_destiny: None, + }), Err(err) => { - println!("Failed to setup from ELF: {err}"); + warn!("Failed to setup from ELF: {err}"); None } } @@ -127,15 +117,7 @@ pub fn handle_register(handler: &mut EvmHandler<'_, EXT, DB>) handler.execution.call = Arc::new(move |ctx, inputs| { let result = old_handle(ctx, inputs); if let Ok(FrameOrResult::Frame(frame)) = &result { - println!("----"); - println!("Frame created successfully"); - println!("Contract: {}", frame.interpreter().contract.target_address); - println!("Code size: {}", frame.interpreter().bytecode.len()); - let context = riscv_context(frame); - println!("RISC-V context created: {}", context.is_some()); - println!("----"); - call_stack_inner.borrow_mut().push(context); } result @@ -156,33 +138,35 @@ pub fn handle_register(handler: &mut EvmHandler<'_, EXT, DB>) let old_handle = handler.execution.execute_frame.clone(); handler.execution.execute_frame = Arc::new(move |frame, memory, instraction_table, ctx| { let depth = call_stack.borrow().len() - 1; + // use last frame as stack is FIFO let mut result = if let Some(Some(riscv_context)) = call_stack.borrow_mut().last_mut() { - println!( - "\n*[{}] RISC-V Emu Handler (with PC: 0x{:x})", - depth, riscv_context.emu.cpu.pc + debug!( + "=== [FRAME-{}] Contract: {} ============-", + depth, + frame.interpreter().contract.target_address, ); - execute_riscv(riscv_context, frame.interpreter_mut(), memory, ctx)? } else { - println!("\n*[OLD Handler]"); + debug!("=== [OLD Handler] ==================--"); old_handle(frame, memory, instraction_table, ctx)? }; - // if it is return pop the stack. + // if action is return, pop the stack. if result.is_return() { - println!("=== RETURN Frame ==="); call_stack.borrow_mut().pop(); - println!( - "Popped frame from stack. Remaining frames: {}", - call_stack.borrow().len() + + let is_last = call_stack.borrow().is_empty(); + debug!( + "=== [FRAME-{}] Ouput: RETURN {}", + depth, + if is_last { "(last)" } else { "" } ); - // if cross-contract call, copy return data into memory range expected by the parent - if !call_stack.borrow().is_empty() { + if !is_last { if let Some(Some(parent)) = call_stack.borrow_mut().last_mut() { if let Some(return_range) = &parent.returned_data_destiny { if let InterpreterAction::Return { result: res } = &mut result { - // Get allocated memory slice + // get allocated memory slice let return_memory = parent .emu .cpu @@ -190,21 +174,22 @@ pub fn handle_register(handler: &mut EvmHandler<'_, EXT, DB>) .get_dram_slice(return_range.clone()) .expect("unable to get memory from return range"); - println!("Return data: {:?}", res.output); - println!("Memory range: {:?}", return_range); - println!("Memory size: {}", return_memory.len()); + debug!("- Return data: {:?}", res.output); + debug!("- Memory range: {:?}", return_range); + debug!("- Memory size: {}", return_memory.len()); - // Write return data to parent's memory + // write return data to parent's memory if res.output.len() == return_memory.len() { - println!("Copying output to memory"); return_memory.copy_from_slice(&res.output); + } else { + warn!("Unexpected output size!") } } } } } } - println!("Frame({}) Gas: {:#?}", depth, frame.interpreter().gas); + debug!("=== [Frame-{}] {:#?}", depth, frame.interpreter().gas); Ok(result) }); @@ -216,15 +201,14 @@ fn execute_riscv( shared_memory: &mut SharedMemory, host: &mut dyn Host, ) -> Result { - println!( - "{} RISC-V execution:\n PC: {:#x}\n Contract: {}\n Return data dst: {:#?}", + debug!( + "{} RISC-V execution: PC: {:#x} Return data dst: {:#?}", if rvemu.emu.cpu.pc == 0x80300000 { "Starting" } else { "Resuming" }, rvemu.emu.cpu.pc, - interpreter.contract.target_address, &rvemu.returned_data_destiny ); @@ -237,7 +221,7 @@ fn execute_riscv( if shared_memory.len() >= data.len() { data.copy_from_slice(shared_memory.slice(0, data.len())); } - println!("Loaded return data: {}", Bytes::copy_from_slice(data)); + debug!("Loaded return data: {}", Bytes::copy_from_slice(data)); } let return_revert = |interpreter: &mut Interpreter, gas_used: u64| { @@ -260,10 +244,10 @@ fn execute_riscv( let t0: u64 = emu.cpu.xregs.read(5); let Ok(syscall) = Syscall::try_from(t0 as u8) else { - println!("Unhandled syscall: {:?}", t0); + warn!("Unhandled syscall: {:?}", t0); return return_revert(interpreter, interpreter.gas.spent()); }; - println!("> [Syscall::{} - {:#04x}]", syscall, t0); + debug!("[Syscall::{} - {:#04x}]", syscall, t0); match syscall { Syscall::Return => { @@ -271,14 +255,13 @@ fn execute_riscv( let ret_size: u64 = emu.cpu.xregs.read(11); let r55_gas = r55_gas_used(&emu.cpu.inst_counter); - println!("> total R55 gas: {}", r55_gas); + debug!("> total R55 gas: {}", r55_gas); // RETURN logs the gas of the whole risc-v instruction set syscall_gas!(interpreter, r55_gas); let data_bytes = dram_slice(emu, ret_offset, ret_size)?; - println!("interpreter remaining gas: {}", interpreter.gas.remaining()); return Ok(InterpreterAction::Return { result: InterpreterResult { result: InstructionResult::Return, @@ -289,14 +272,14 @@ fn execute_riscv( } Syscall::SLoad => { let key: u64 = emu.cpu.xregs.read(10); - println!( - "SLOAD ({}) - Key: {}", + debug!( + "> SLOAD ({}) - Key: {}", interpreter.contract.target_address, key ); match host.sload(interpreter.contract.target_address, U256::from(key)) { Some((value, is_cold)) => { - println!( - "SLOAD ({}) - Value: {}", + debug!( + "> SLOAD ({}) - Value: {}", interpreter.contract.target_address, value ); let limbs = value.as_limbs(); @@ -361,8 +344,8 @@ fn execute_riscv( // Store where return data should go let ret_offset = emu.cpu.xregs.read(16); let ret_size = emu.cpu.xregs.read(17); - println!( - "Return data will be written to: {}..{}", + debug!( + "> Return data will be written to: {}..{}", ret_offset, ret_offset + ret_size ); @@ -396,11 +379,11 @@ fn execute_riscv( let call_gas_limit = interpreter.gas.remaining(); syscall_gas!(interpreter, call_gas_limit); - println!("Call context:"); - println!(" Caller: {}", interpreter.contract.target_address); - println!(" Target Address: {}", addr); - println!(" Value: {}", value); - println!(" Calldata: {:?}", calldata); + debug!("> Call context:"); + debug!(" - Caller: {}", interpreter.contract.target_address); + debug!(" - Target Address: {}", addr); + debug!(" - Value: {}", value); + debug!(" - Calldata: {:?}", calldata); return Ok(InterpreterAction::Call { inputs: Box::new(CallInputs { input: calldata, @@ -567,11 +550,11 @@ fn execute_riscv( } } Ok(_) => { - println!("Successful instruction at PC: {:#x}", emu.cpu.pc); + debug!("Successful instruction at PC: {:#x}", emu.cpu.pc); continue; } Err(e) => { - println!("Execution error: {:#?}", e); + debug!("Execution error: {:#?}", e); syscall_gas!(interpreter, r55_gas_used(&emu.cpu.inst_counter)); return return_revert(interpreter, interpreter.gas.spent()); } diff --git a/r55/src/gas.rs b/r55/src/gas.rs index 099bab8..e4c840d 100644 --- a/r55/src/gas.rs +++ b/r55/src/gas.rs @@ -1,3 +1,5 @@ +use tracing::debug; + // Standard EVM operation costs pub const SLOAD_COLD: u64 = 2100; pub const SLOAD_WARM: u64 = 100; @@ -15,14 +17,12 @@ pub const CALL_BASE: u64 = 100; #[macro_export] macro_rules! syscall_gas { ($interpreter:expr, $gas_cost:expr $(,)?) => {{ - let remaining_before = $interpreter.gas.remaining(); let gas_cost = $gas_cost; - println!("> About to log gas costs:"); - println!(" - Operation cost: {}", gas_cost); - println!(" - Gas remaining: {}", remaining_before); - println!(" - Gas limit: {}", $interpreter.gas.limit()); - println!(" - Gas spent: {}", $interpreter.gas.spent()); + debug!("> About to record gas costs:"); + debug!(" - Gas limit: {}", $interpreter.gas.limit()); + debug!(" - Gas prev spent: {}", $interpreter.gas.spent()); + debug!(" - Operation cost: {}", gas_cost); if !$interpreter.gas.record_cost(gas_cost) { eprintln!("OUT OF GAS"); @@ -35,8 +35,8 @@ macro_rules! syscall_gas { }); } - println!("> Gas recorded successfully:"); - println!(" - Gas remaining: {}", remaining_before); - println!(" - Gas spent: {}", $interpreter.gas.spent()); + debug!("> Gas recorded successfully:"); + debug!(" - Gas remaining: {}", $interpreter.gas.remaining()); + debug!(" - Gas spent: {}", $interpreter.gas.spent()); }}; } diff --git a/r55/src/lib.rs b/r55/src/lib.rs index 26c2cda..7e722b2 100644 --- a/r55/src/lib.rs +++ b/r55/src/lib.rs @@ -9,9 +9,10 @@ use alloy_primitives::Bytes; use std::fs::File; use std::io::Read; use std::process::Command; +use tracing::{error, info}; fn compile_runtime(path: &str) -> eyre::Result> { - log::info!("Compiling runtime: {}", path); + info!("Compiling runtime: {}", path); let status = Command::new("cargo") .arg("+nightly-2024-02-01") .arg("build") @@ -28,10 +29,10 @@ fn compile_runtime(path: &str) -> eyre::Result> { .expect("Failed to execute cargo command"); if !status.success() { - log::error!("Cargo command failed with status: {}", status); + error!("Cargo command failed with status: {}", status); std::process::exit(1); } else { - log::info!("Cargo command completed successfully"); + info!("Cargo command completed successfully"); } let path = format!( @@ -56,7 +57,7 @@ fn compile_runtime(path: &str) -> eyre::Result> { pub fn compile_deploy(path: &str) -> eyre::Result> { compile_runtime(path)?; - log::info!("Compiling deploy: {}", path); + info!("Compiling deploy: {}", path); let status = Command::new("cargo") .arg("+nightly-2024-02-01") .arg("build") @@ -73,10 +74,10 @@ pub fn compile_deploy(path: &str) -> eyre::Result> { .expect("Failed to execute cargo command"); if !status.success() { - log::error!("Cargo command failed with status: {}", status); + error!("Cargo command failed with status: {}", status); std::process::exit(1); } else { - log::info!("Cargo command completed successfully"); + info!("Cargo command completed successfully"); } let path = format!( diff --git a/r55/src/test_utils.rs b/r55/src/test_utils.rs index 82c37d5..2e5983f 100644 --- a/r55/src/test_utils.rs +++ b/r55/src/test_utils.rs @@ -8,7 +8,18 @@ static INIT: Once = Once::new(); pub fn initialize_logger() { INIT.call_once(|| { - env_logger::builder().is_test(true).try_init().unwrap(); + let log_level = std::env::var("RUST_LOG").unwrap_or("debug".to_owned()); + let tracing_sub = tracing_subscriber::fmt() + .with_max_level(tracing::Level::DEBUG) + .with_env_filter(tracing_subscriber::EnvFilter::new(&format!( + "{}", + log_level + ))) + .with_target(false) + // .without_time() + .finish(); + tracing::subscriber::set_global_default(tracing_sub) + .expect("Setting tracing subscriber failed"); }); } diff --git a/r55/tests/e2e.rs b/r55/tests/e2e.rs index 1a4bc3b..30ad3fb 100644 --- a/r55/tests/e2e.rs +++ b/r55/tests/e2e.rs @@ -9,6 +9,7 @@ use revm::{ primitives::{address, Address}, InMemoryDB, }; +use tracing::{debug, error, info}; const ERC20_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../erc20"); @@ -42,31 +43,39 @@ fn erc20() { let mut complete_calldata_x_balance = selector_x_balance.to_vec(); complete_calldata_x_balance.append(&mut calldata_x_balance); - println!("\n----------------------------------------------------------"); - println!("-- MINT TX -----------------------------------------------"); - println!("----------------------------------------------------------"); - println!( - " > TX Calldata: {:#?\n}", + info!("----------------------------------------------------------"); + info!("-- MINT TX -----------------------------------------------"); + info!("----------------------------------------------------------"); + debug!( + "Tx Calldata:\n> {:#?}", Bytes::from(complete_calldata_mint.clone()) ); - run_tx(&mut db, &addr1, complete_calldata_mint.clone()).unwrap(); - println!("\n----------------------------------------------------------"); - println!("-- BALANCE OF TX -----------------------------------------"); - println!("----------------------------------------------------------"); - println!( - " > TX Calldata: {:#?}\n", + match run_tx(&mut db, &addr1, complete_calldata_mint.clone()) { + Ok(res) => info!("Success! {}", res), + Err(e) => error!("Error when executing tx! {:#?}", e), + }; + + info!("----------------------------------------------------------"); + info!("-- BALANCE OF TX -----------------------------------------"); + info!("----------------------------------------------------------"); + debug!( + "Tx Calldata:\n> {:#?}", Bytes::from(complete_calldata_balance.clone()) ); - run_tx(&mut db, &addr1, complete_calldata_balance.clone()).unwrap(); - println!("\n----------------------------------------------------------"); - println!("-- X-CONTRACT BALANCE OF TX ------------------------------"); - println!("----------------------------------------------------------"); - println!( - " > TX Calldata: {:#?}\n", + match run_tx(&mut db, &addr1, complete_calldata_balance.clone()) { + Ok(res) => info!("Success! {}", res), + Err(e) => error!("Error when executing tx! {:#?}", e), + }; + + info!("----------------------------------------------------------"); + info!("-- X-CONTRACT BALANCE OF TX ------------------------------"); + info!("----------------------------------------------------------"); + debug!( + "Tx calldata:\n> {:#?}", Bytes::from(complete_calldata_x_balance.clone()) ); match run_tx(&mut db, &addr2, complete_calldata_x_balance.clone()) { - Ok(res) => log::info!("res: {:#?}", res), - Err(e) => log::error!("{:#?}", e), - }; + Ok(res) => info!("Success! {}", res), + Err(e) => error!("Error when executing tx! {:#?}", e), + } } From 267b063e0b8ac8856666c9ee2a381f951395eb34 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Sat, 14 Dec 2024 00:39:28 +0100 Subject: [PATCH 03/11] fix: info as default log level --- .env | 1 - r55/src/gas.rs | 2 -- r55/src/test_utils.rs | 2 +- 3 files changed, 1 insertion(+), 4 deletions(-) delete mode 100644 .env diff --git a/.env b/.env deleted file mode 100644 index 93edc8b..0000000 --- a/.env +++ /dev/null @@ -1 +0,0 @@ -RUST_LOG=DEBUG diff --git a/r55/src/gas.rs b/r55/src/gas.rs index e4c840d..ed746f6 100644 --- a/r55/src/gas.rs +++ b/r55/src/gas.rs @@ -1,5 +1,3 @@ -use tracing::debug; - // Standard EVM operation costs pub const SLOAD_COLD: u64 = 2100; pub const SLOAD_WARM: u64 = 100; diff --git a/r55/src/test_utils.rs b/r55/src/test_utils.rs index 2e5983f..4f84161 100644 --- a/r55/src/test_utils.rs +++ b/r55/src/test_utils.rs @@ -8,7 +8,7 @@ static INIT: Once = Once::new(); pub fn initialize_logger() { INIT.call_once(|| { - let log_level = std::env::var("RUST_LOG").unwrap_or("debug".to_owned()); + let log_level = std::env::var("RUST_LOG").unwrap_or("INFO".to_owned()); let tracing_sub = tracing_subscriber::fmt() .with_max_level(tracing::Level::DEBUG) .with_env_filter(tracing_subscriber::EnvFilter::new(&format!( From f027af5f2944b482cf680624cc214fcb7598e40a Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Sat, 14 Dec 2024 00:47:37 +0100 Subject: [PATCH 04/11] fix: readme --- README.md | 3212 ++---------------------------------- contract-derive/src/lib.rs | 14 - r55/src/exec.rs | 6 +- 3 files changed, 114 insertions(+), 3118 deletions(-) diff --git a/README.md b/README.md index 7edd718..f86613e 100644 --- a/README.md +++ b/README.md @@ -1,3159 +1,169 @@ -i am trying to understand the r55 project. here u have an intro of what it does: -``` -R55 is an experimental Ethereum Execution Environment that seamlessly integrates RISCV smart contracts alongside traditional EVM smart contracts. This dual support operates over the same Ethereum state, and communication happens via ABI-encoded calls. - -On the high level, R55 enables the use of pure Rust smart contracts, opening the door for a vast Rust developer community to engage in Ethereum development with minimal barriers to entry, and increasing language and compiler diversity. - -On the low level, RISCV code allows for optimization opportunities distinct from the EVM, including the use of off-the-shelf ASICs. This potential for performance gains can be particularly advantageous in specialized domains. -``` -and its architecture: -""" -# Architecture - -The compiler uses `rustc`, `llvm`, -[eth-riscv-syscalls](https://github.com/r55-eth/r55/tree/main/eth-riscv-syscalls), -[eth-riscv-runtime](https://github.com/r55-eth/r55/tree/main/eth-riscv-runtime) -and [riscv-rt](https://github.com/rust-embedded/riscv/tree/master/riscv-rt) to -compile and link ELF binaries with low-level syscalls to be executed by -[rvemu-r55](https://github.com/r55-eth/rvemu): - -```mermaid -graph TD; - RustContract[Rust contract] --> CompiledContract[compiled contract] - rustc --> CompiledContract - llvm --> CompiledContract - EthRiscVSyscalls[eth-riscv-syscalls] --> CompiledContract - EthRiscVRuntime1[eth-riscv-runtime] --> CompiledContract - CompiledContract --> LinkedRuntimeBytecode[linked runtime bytecode] - EthRiscVRuntime2[eth-riscv-runtime] --> LinkedRuntimeBytecode - riscv_rt[riscv-rt] --> LinkedRuntimeBytecode - LinkedRuntimeBytecode --> LinkedInitBytecode[linked init bytecode] - EthRiscVRuntime3[eth-riscv-runtime] --> LinkedInitBytecode -``` - -The execution environment depends on [revm](https://github.com/bluealloy/revm), -and relies on the [rvemu-r55](https://github.com/r55-eth/rvemu) RISCV -interpreter and -[eth-riscv-runtime](https://github.com/r55-eth/r55/tree/main/eth-riscv-runtime). - -```mermaid -graph TD; - revm --> revm-r55 - rvemu-r55 --> revm-r55 - eth-riscv-runtime --> revm-r55 -``` -""" - -i want to impl cross-contract call capabilities, and to do so, i first looked at the `contract-derive` crate, which converts the impl of a struct into a smart contract. -so i implemented a macro similar to the contract one, but to create interfaces from a trait, so that i could follow a similar approach for the cross-contract call capabilities. - -here you have the relevant code: -```contract-derive/src/lib.rs -extern crate proc_macro; -use alloy_core::primitives::keccak256; -use alloy_sol_types::SolValue; -use proc_macro::TokenStream; -use quote::{format_ident, quote}; -use syn::{parse_macro_input, Data, DeriveInput, Fields, ImplItem, ItemImpl, ItemTrait, TraitItem}; -use syn::{FnArg, ReturnType}; - -#[proc_macro_derive(Event, attributes(indexed))] -pub fn event_derive(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - let name = &input.ident; - - let fields = if let Data::Struct(data) = &input.data { - if let Fields::Named(fields) = &data.fields { - &fields.named - } else { - panic!("Event must have named fields"); - } - } else { - panic!("Event must be a struct"); - }; - - // Collect iterators into vectors - let field_names: Vec<_> = fields.iter().map(|f| &f.ident).collect(); - let field_types: Vec<_> = fields.iter().map(|f| &f.ty).collect(); - let indexed_fields: Vec<_> = fields - .iter() - .filter(|f| f.attrs.iter().any(|attr| attr.path.is_ident("indexed"))) - .map(|f| &f.ident) - .collect(); - - let expanded = quote! { - impl #name { - const NAME: &'static str = stringify!(#name); - const INDEXED_FIELDS: &'static [&'static str] = &[ - #(stringify!(#indexed_fields)),* - ]; - - pub fn new(#(#field_names: #field_types),*) -> Self { - Self { - #(#field_names),* - } - } - } - - impl eth_riscv_runtime::log::Event for #name { - fn encode_log(&self) -> (alloc::vec::Vec, alloc::vec::Vec<[u8; 32]>) { - use alloy_sol_types::SolValue; - use alloy_core::primitives::{keccak256, B256}; - use alloc::vec::Vec; - - let mut signature = Vec::new(); - signature.extend_from_slice(Self::NAME.as_bytes()); - signature.extend_from_slice(b"("); - - let mut first = true; - let mut topics = alloc::vec![B256::default()]; - let mut data = Vec::new(); - - #( - if !first { signature.extend_from_slice(b","); } - first = false; - - signature.extend_from_slice(self.#field_names.sol_type_name().as_bytes()); - let encoded = self.#field_names.abi_encode(); - - let field_name = stringify!(#field_names); - if Self::INDEXED_FIELDS.contains(&field_name) && topics.len() < 4 { - topics.push(B256::from_slice(&encoded)); - } else { - data.extend_from_slice(&encoded); - } - )* - - signature.extend_from_slice(b")"); - topics[0] = B256::from(keccak256(&signature)); - - (data, topics.iter().map(|t| t.0).collect()) - } - } - }; - - TokenStream::from(expanded) -} - -#[proc_macro_attribute] -pub fn show_streams(attr: TokenStream, item: TokenStream) -> TokenStream { - println!("attr: \"{}\"", attr.to_string()); - println!("item: \"{}\"", item.to_string()); - item -} - -#[proc_macro_attribute] -pub fn contract(_attr: TokenStream, item: TokenStream) -> TokenStream { - let input = parse_macro_input!(item as ItemImpl); - let struct_name = if let syn::Type::Path(type_path) = &*input.self_ty { - &type_path.path.segments.first().unwrap().ident - } else { - panic!("Expected a struct."); - }; - - let mut public_methods = Vec::new(); - - // Iterate over the items in the impl block to find pub methods - for item in input.items.iter() { - if let ImplItem::Method(method) = item { - if let syn::Visibility::Public(_) = method.vis { - public_methods.push(method.clone()); - } - } - } - - let match_arms: Vec<_> = public_methods.iter().enumerate().map(|(_, method)| { - let method_name = &method.sig.ident; - let method_selector = u32::from_be_bytes( - keccak256( - method_name.to_string() - )[..4].try_into().unwrap_or_default() - ); - let arg_types: Vec<_> = method.sig.inputs.iter().skip(1).map(|arg| { - if let FnArg::Typed(pat_type) = arg { - let ty = &*pat_type.ty; - quote! { #ty } - } else { - panic!("Expected typed arguments"); - } - }).collect(); - - let arg_names: Vec<_> = (0..method.sig.inputs.len() - 1).map(|i| format_ident!("arg{}", i)).collect(); - let checks = if !is_payable(&method) { - quote! { - if eth_riscv_runtime::msg_value() > U256::from(0) { - revert(); - } - } - } else { - quote! {} - }; - // Check if the method has a return type - let return_handling = match &method.sig.output { - ReturnType::Default => { - // No return value - quote! { - self.#method_name(#( #arg_names ),*); - } - } - ReturnType::Type(_, return_type) => { - // Has return value - quote! { - let result: #return_type = self.#method_name(#( #arg_names ),*); - let result_bytes = result.abi_encode(); - let result_size = result_bytes.len() as u64; - let result_ptr = result_bytes.as_ptr() as u64; - return_riscv(result_ptr, result_size); - } - } - }; - - quote! { - #method_selector => { - let (#( #arg_names ),*) = <(#( #arg_types ),*)>::abi_decode(calldata, true).unwrap(); - #checks - #return_handling - } - } - }).collect(); - - let emit_helper = quote! { - #[macro_export] - macro_rules! get_type_signature { - ($arg:expr) => { - $arg.sol_type_name().as_bytes() - }; - } - - #[macro_export] - macro_rules! emit { - ($event:ident, $($field:expr),*) => {{ - use alloy_sol_types::SolValue; - use alloy_core::primitives::{keccak256, B256, U256, I256}; - use alloc::vec::Vec; - - let mut signature = alloc::vec![]; - signature.extend_from_slice($event::NAME.as_bytes()); - signature.extend_from_slice(b"("); - - let mut first = true; - let mut topics = alloc::vec![B256::default()]; - let mut data = Vec::new(); - - $( - if !first { signature.extend_from_slice(b","); } - first = false; - - signature.extend_from_slice(get_type_signature!($field)); - let encoded = $field.abi_encode(); - - let field_ident = stringify!($field); - if $event::INDEXED_FIELDS.contains(&field_ident) && topics.len() < 4 { - topics.push(B256::from_slice(&encoded)); - } else { - data.extend_from_slice(&encoded); - } - )* - - signature.extend_from_slice(b")"); - topics[0] = B256::from(keccak256(&signature)); - - if !data.is_empty() { - eth_riscv_runtime::emit_log(&data, &topics); - } else if topics.len() > 1 { - let data = topics.pop().unwrap(); - eth_riscv_runtime::emit_log(data.as_ref(), &topics); - } - }}; - } - }; - - // Generate the call method implementation - let call_method = quote! { - use alloy_sol_types::SolValue; - use eth_riscv_runtime::*; - - #emit_helper - impl Contract for #struct_name { - fn call(&self) { - self.call_with_data(&msg_data()); - } - - fn call_with_data(&self, calldata: &[u8]) { - let selector = u32::from_be_bytes([calldata[0], calldata[1], calldata[2], calldata[3]]); - let calldata = &calldata[4..]; - - match selector { - #( #match_arms )* - _ => revert(), - } - - return_riscv(0, 0); - } - } - - #[eth_riscv_runtime::entry] - fn main() -> ! - { - let contract = #struct_name::default(); - contract.call(); - eth_riscv_runtime::return_riscv(0, 0) - } - }; - - let output = quote! { - #input - #call_method - }; - - TokenStream::from(output) -} - -// Empty macro to mark a method as payable -#[proc_macro_attribute] -pub fn payable(_attr: TokenStream, item: TokenStream) -> TokenStream { - item -} - -// Check if a method is tagged with the payable attribute -fn is_payable(method: &syn::ImplItemMethod) -> bool { - method.attrs.iter().any(|attr| { - if let Ok(syn::Meta::Path(path)) = attr.parse_meta() { - if let Some(segment) = path.segments.first() { - return segment.ident == "payable"; - } - } - false - }) -} +# R55 -#[proc_macro_attribute] -pub fn interface(_attr: TokenStream, item: TokenStream) -> TokenStream { - // DEBUG - println!("\n=== Input Trait ==="); - println!("{}", item.to_string()); +R55 is an experimental Ethereum Execution Environment that seamlessly +integrates RISCV smart contracts alongside traditional EVM smart contracts. +This dual support operates over the same Ethereum state, and communication +happens via ABI-encoded calls. - let input = parse_macro_input!(item as ItemTrait); - let trait_name = &input.ident; +On the high level, R55 enables the use of pure Rust smart contracts, opening +the door for a vast Rust developer community to engage in Ethereum development +with minimal barriers to entry, and increasing language and compiler diversity. - let method_impls: Vec<_> = input - .items - .iter() - .map(|item| { - if let TraitItem::Method(method) = item { - let method_name = &method.sig.ident; - let selector_bytes = keccak256(method_name.to_string())[..4] - .try_into() - .unwrap_or_default(); - let method_selector = u32::from_be_bytes(selector_bytes); +On the low level, RISCV code allows for optimization opportunities distinct +from the EVM, including the use of off-the-shelf ASICs. This potential for +performance gains can be particularly advantageous in specialized domains. - // DEBUG - println!("\n=== Processing Method ==="); - println!("Method name: {}", method_name); - println!("Selector bytes: {:02x?}", selector_bytes); - println!("Selector u32: {}", method_selector); +# Off-the-shelf tooling - // Extract argument types and names, skipping self - let arg_types: Vec<_> = method - .sig - .inputs - .iter() - .skip(1) - .map(|arg| { - if let FnArg::Typed(pat_type) = arg { - let ty = &*pat_type.ty; - quote! { #ty } - } else { - panic!("Expected typed arguments"); - } - }) - .collect(); - let arg_names: Vec<_> = (0..method.sig.inputs.len() - 1) - .map(|i| format_ident!("arg{}", i)) - .collect(); +R55 relies on standard tooling that programmers are used to, such as Rust, +Cargo and LLVM. This directly enables tooling such as linters, static +analyzers, testing, fuzzing, and formal verification tools to be applied to +these smart contracts without extra development and research. - // Get the return type - let return_type = match &method.sig.output { - ReturnType::Default => quote! { () }, - ReturnType::Type(_, ty) => - quote! { #ty }, - }; +# Pure & Clean Rust Smart Contracts - // Get the return size - let return_size = match &method.sig.output { - ReturnType::Default => quote! { 0_u64 }, - // TODO: Figure out how to use SolType to figure out the return size - ReturnType::Type(_, ty) => quote! {32_u64} +Differently from other platforms that offer Rust smart contracts, R55 lets the +user code in [no\_std] Rust without weird edges. The code below implements a +basic ERC20 token with infinite minting for testing. +Because the `struct` and `impl` are just Rust code, the user can write normal +tests and run them natively (as long as they don't need Ethereum host +functions). Note that [alloy-rs](https://github.com/alloy-rs/) types work +out-of-the-box. - }; - - // Generate calldata with different encoding depending on # of args - let args_encoding = if arg_names.is_empty() { - quote! { - let mut complete_calldata = Vec::with_capacity(4); - complete_calldata.extend_from_slice(&[ - #method_selector.to_be_bytes()[0], - #method_selector.to_be_bytes()[1], - #method_selector.to_be_bytes()[2], - #method_selector.to_be_bytes()[3], - ]); - } - } else if arg_names.len() == 1 { - quote! { - let mut args_calldata = #(#arg_names),*.abi_encode(); - let mut complete_calldata = Vec::with_capacity(4 + args_calldata.len()); - complete_calldata.extend_from_slice(&[ - #method_selector.to_be_bytes()[0], - #method_selector.to_be_bytes()[1], - #method_selector.to_be_bytes()[2], - #method_selector.to_be_bytes()[3], - ]); - complete_calldata.append(&mut args_calldata); - } - } else { - quote! { - let mut args_calldata = (#(#arg_names),*).abi_encode(); - let mut complete_calldata = Vec::with_capacity(4 + args_calldata.len()); - complete_calldata.extend_from_slice(&[ - #method_selector.to_be_bytes()[0], - #method_selector.to_be_bytes()[1], - #method_selector.to_be_bytes()[2], - #method_selector.to_be_bytes()[3], - ]); - complete_calldata.append(&mut args_calldata); - } - }; - - Some(quote! { - pub fn #method_name(&self, #(#arg_names: #arg_types),*) -> Option<#return_type> { - use alloy_sol_types::SolValue; - use alloc::vec::Vec; - - #args_encoding - - // Make the call - let result = eth_riscv_runtime::call_contract( - self.address, - 0_u64, - &complete_calldata, - #return_size as u64 - )?; - - // Decode result - <#return_type>::abi_decode(&result, true).ok() - } - }) - } else { - panic!("Expected methods arguments"); - } - }) - .collect(); - - let expanded = quote! { - pub struct #trait_name { - address: Address, - } - - impl #trait_name { - pub fn new(address: Address) -> Self { - Self { address } - } - - #(#method_impls)* - } - }; - - // DEBUG - println!("\n=== Generated Code ==="); - println!("{:#}", expanded.to_string().replace(';', ";\n")); - - TokenStream::from(expanded) -} -``` - -thanks to those macros we can define a contract like: -```erc20/src/lib.rs +```rust #![no_std] #![no_main] use core::default::Default; -use contract_derive::{contract, interface, payable, Event}; +use contract_derive::contract; use eth_riscv_runtime::types::Mapping; -use alloy_core::primitives::{address, Address, U256}; - -extern crate alloc; -use alloc::{string::String, vec::Vec}; +use alloy_core::primitives::Address; #[derive(Default)] pub struct ERC20 { - balances: Mapping, - allowances: Mapping>, - total_supply: U256, - name: String, - symbol: String, - decimals: u8, -} -#[derive(Event)] -pub struct DebugCalldata { - #[indexed] - pub target: Address, - pub calldata: Vec, -} - -#[interface] -trait IERC20 { - fn balance_of(&self, owner: Address) -> u64; -} - -#[derive(Event)] -pub struct Transfer { - #[indexed] - pub from: Address, - #[indexed] - pub to: Address, - pub value: u64, -} - -#[derive(Event)] -pub struct Mint { - #[indexed] - pub from: Address, - #[indexed] - pub to: Address, - pub value: u64, + balance: Mapping, } #[contract] impl ERC20 { - pub fn x_balance_of(&self, owner: Address, target: Address) -> u64 { - let token = IERC20::new(target); - match token.balance_of(owner) { - Some(balance) => balance, - _ => eth_riscv_runtime::revert(), - } - } - pub fn balance_of(&self, owner: Address) -> u64 { - self.balances.read(owner) + self.balance.read(owner) } - pub fn transfer(&self, to: Address, value: u64) -> bool { - let from = msg_sender(); - let from_balance = self.balances.read(from); - let to_balance = self.balances.read(to); + pub fn transfer(&self, from: Address, to: Address, value: u64) { + let from_balance = self.balance.read(from); + let to_balance = self.balance.read(to); if from == to || from_balance < value { revert(); } - self.balances.write(from, from_balance - value); - self.balances.write(to, to_balance + value); - - log::emit(Transfer::new(from, to, value)); - true - } - - pub fn approve(&self, spender: Address, value: u64) -> bool { - let spender_allowances = self.allowances.read(msg_sender()); - spender_allowances.write(spender, value); - true - } - - pub fn transfer_from(&self, sender: Address, recipient: Address, amount: u64) -> bool { - let allowance = self.allowances.read(sender).read(msg_sender()); - let sender_balance = self.balances.read(sender); - let recipient_balance = self.balances.read(recipient); - - self.allowances - .read(sender) - .write(msg_sender(), allowance - amount); - self.balances.write(sender, sender_balance - amount); - self.balances.write(recipient, recipient_balance + amount); - - true - } - - pub fn total_supply(&self) -> U256 { - self.total_supply - } - - pub fn allowance(&self, owner: Address, spender: Address) -> u64 { - self.allowances.read(owner).read(spender) + self.balance.write(from, from_balance - value); + self.balance.write(to, to_balance + value); } - #[payable] - pub fn mint(&self, to: Address, value: u64) -> bool { - let owner = msg_sender(); - - let to_balance = self.balances.read(to); - self.balances.write(to, to_balance + value); - log::emit(Transfer::new( - address!("0000000000000000000000000000000000000000"), - to, - value, - )); - true + pub fn mint(&self, to: Address, value: u64) { + let to_balance = self.balance.read(to); + self.balance.write(to, to_balance + value); } } ``` -I am quite happy with how this came out. However, this is just the API that developers will use to interact with other contracts. I still needed to implement the cross contract call capabilities. -To do so, r55 uses a hybrid execution environment leveraging risc-v when possible and via syscalls that use revm. - -Here u have the syscalls: -```eth-riscv-syscall/src/lib.rs -#![no_std] - -extern crate alloc; +The macro `#[contract]` above is the only special treatment the user needs to +apply to their code. Specifically, it is responsible for the init code +(deployer), and for creating the function dispatcher based on the given +methods. +Note that Rust `pub` methods are exposed as public functions in the deployed +contract, similarly to Solidity's `public` functions. -mod error; -pub use error::Error; +# Client Integration -macro_rules! syscalls { - ($(($num:expr, $identifier:ident, $name:expr)),* $(,)?) => { - #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] - #[repr(u8)] - pub enum Syscall { - $($identifier = $num),* - } +R55 is a fork of [revm](https://github.com/bluealloy/revm) without any API +changes. Therefore it can be used seamlessly in Anvil/Reth to deploy a +testnet/network with support to RISCV smart contracts. +Nothing has to be changed in how transactions are handled or created. - impl core::fmt::Display for Syscall { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!(f, "{}", match self { - $(Syscall::$identifier => $name),* - }) - } - } +# Relevant Links - impl core::str::FromStr for Syscall { - type Err = Error; - fn from_str(input: &str) -> Result { - match input { - $($name => Ok(Syscall::$identifier)),*, - name => Err(Error::ParseError { input: alloc::string::String::from(name).into() }), - } - } - } +- [revm-R55](https://github.com/r0qs/revm) +- [rvemu-R55](https://github.com/r55-eth/rvemu) +- [R55 Ethereum Runtime](https://github.com/r55-eth/r55/tree/main/eth-riscv-runtime) +- [R55 Compiler](https://github.com/r55-eth/r55/tree/main/r55) - impl From for u8 { - fn from(syscall: Syscall) -> Self { - syscall as Self - } - } +# Prerequisites - impl core::convert::TryFrom for Syscall { - type Error = Error; - fn try_from(value: u8) -> Result { - match value { - $($num => Ok(Syscall::$identifier)),*, - num => Err(Error::UnknownOpcode(num)), - } - } - } - } -} +## macOS -// Generate `Syscall` enum with supported syscalls and their ids. -// -// The opcode for each syscall matches the corresponding EVM opcode, -// as described on https://www.evm.codes. -// -// t0: 0x20, opcode for keccak256, a0: offset, a1: size, returns keccak256 hash -// t0: 0x32, opcode for origin, returns an address -// t0: 0x33, opcode for caller, returns an address -// t0: 0x34, opcode for callvalue, a0: first limb, a1: second limb, a2: third limb, a3: fourth limb, returns 256-bit value -// t0: 0x3A, opcode for gasprice, returns 256-bit value -// t0: 0x54, opcode for sload, a0: storage key, returns 64-bit value in a0 -// t0: 0x55, opcode for sstore, a0: storage key, a1: storage value, returns nothing -// t0: 0xf1, opcode for call, args: TODO -// t0: 0xf3, opcode for return, a0: memory address of data, a1: length of data in bytes, doesn't return -// t0: 0xfd, opcode for revert, doesn't return -syscalls!( - (0x20, Keccak256, "keccak256"), - (0x32, Origin, "origin"), - (0x33, Caller, "caller"), - (0x34, CallValue, "callvalue"), - (0x3A, GasPrice, "gasprice"), - (0x42, Timestamp, "timestamp"), - (0x43, Number, "number"), - (0x45, GasLimit, "gaslimit"), - (0x46, ChainId, "chainid"), - (0x48, BaseFee, "basefee"), - (0x54, SLoad, "sload"), - (0x55, SStore, "sstore"), - (0xf1, Call, "call"), - (0xf3, Return, "return"), - (0xfd, Revert, "revert"), - (0xA0, Log, "log"), -); +```shell +brew install riscv-gnu-toolchain gettext ``` -and the runtime: -```eth-riscv-runtime/src/lib.rs -#![no_std] -#![no_main] -#![feature(alloc_error_handler, maybe_uninit_write_slice, round_char_boundary)] - -use alloc::vec::Vec; -use alloy_core::primitives::{Address, Bytes, B256, U256}; -use core::arch::asm; -use core::panic::PanicInfo; -use core::slice; -pub use riscv_rt::entry; - -mod alloc; -pub mod block; -pub mod tx; -pub mod types; - -pub mod log; -pub use log::{emit_log, Event}; - -pub mod call; -pub use call::call_contract; - -const CALLDATA_ADDRESS: usize = 0x8000_0000; - -pub trait Contract { - fn call(&self); - fn call_with_data(&self, calldata: &[u8]); -} - -pub unsafe fn slice_from_raw_parts(address: usize, length: usize) -> &'static [u8] { - slice::from_raw_parts(address as *const u8, length) -} - -#[panic_handler] -unsafe fn panic(_panic: &PanicInfo<'_>) -> ! { - static mut IS_PANICKING: bool = false; - - if !IS_PANICKING { - IS_PANICKING = true; - - revert(); - // TODO with string - //print!("{panic}\n"); - } else { - revert(); - // TODO with string - //print_str("Panic handler has panicked! Things are very dire indeed...\n"); - } -} - -use eth_riscv_syscalls::Syscall; - -pub fn return_riscv(addr: u64, offset: u64) -> ! { - unsafe { - asm!("ecall", in("a0") addr, in("a1") offset, in("t0") u8::from(Syscall::Return)); - } - unreachable!() -} - -pub fn sload(key: u64) -> U256 { - let first: u64; - let second: u64; - let third: u64; - let fourth: u64; - unsafe { - asm!("ecall", lateout("a0") first, lateout("a1") second, lateout("a2") third, lateout("a3") fourth, in("a0") key, in("t0") u8::from(Syscall::SLoad)); - } - U256::from_limbs([first, second, third, fourth]) -} - -pub fn sstore(key: u64, value: U256) { - let limbs = value.as_limbs(); - unsafe { - asm!("ecall", in("a0") key, in("a1") limbs[0], in("a2") limbs[1], in("a3") limbs[2], in("a4") limbs[3], in("t0") u8::from(Syscall::SStore)); - } -} - -pub fn call( - addr: Address, - value: u64, - data_offset: u64, - data_size: u64, - res_offset: u64, - res_size: u64, -) { - let addr: U256 = addr.into_word().into(); - let addr = addr.as_limbs(); - unsafe { - asm!( - "ecall", - in("a0") addr[0], - in("a1") addr[1], - in("a2") addr[2], - in("a3") value, - in("a4") data_offset, - in("a5") data_size, - in("a6") res_offset, - in("a7") res_size, - in("t0") u8::from(Syscall::Call) - ); - } -} - -pub fn revert() -> ! { - unsafe { - asm!("ecall", in("t0") u8::from(Syscall::Revert)); - } - unreachable!() -} - -pub fn keccak256(offset: u64, size: u64) -> B256 { - let first: u64; - let second: u64; - let third: u64; - let fourth: u64; - - unsafe { - asm!( - "ecall", - in("a0") offset, - in("a1") size, - lateout("a0") first, - lateout("a1") second, - lateout("a2") third, - lateout("a3") fourth, - in("t0") u8::from(Syscall::Keccak256) - ); - } - - let mut bytes = [0u8; 32]; - - bytes[0..8].copy_from_slice(&first.to_be_bytes()); - bytes[8..16].copy_from_slice(&second.to_be_bytes()); - bytes[16..24].copy_from_slice(&third.to_be_bytes()); - bytes[24..32].copy_from_slice(&fourth.to_be_bytes()); - - B256::from_slice(&bytes) -} - -pub fn msg_sender() -> Address { - let first: u64; - let second: u64; - let third: u64; - unsafe { - asm!("ecall", lateout("a0") first, lateout("a1") second, lateout("a2") third, in("t0") u8::from(Syscall::Caller)); - } - let mut bytes = [0u8; 20]; - bytes[0..8].copy_from_slice(&first.to_be_bytes()); - bytes[8..16].copy_from_slice(&second.to_be_bytes()); - bytes[16..20].copy_from_slice(&third.to_be_bytes()[..4]); - Address::from_slice(&bytes) -} - -pub fn msg_value() -> U256 { - let first: u64; - let second: u64; - let third: u64; - let fourth: u64; - unsafe { - asm!("ecall", lateout("a0") first, lateout("a1") second, lateout("a2") third, lateout("a3") fourth, in("t0") u8::from(Syscall::CallValue)); - } - U256::from_limbs([first, second, third, fourth]) -} - -pub fn msg_sig() -> [u8; 4] { - let sig = unsafe { slice_from_raw_parts(CALLDATA_ADDRESS + 8, 4) }; - sig.try_into().unwrap() -} - -pub fn msg_data() -> &'static [u8] { - let length = unsafe { slice_from_raw_parts(CALLDATA_ADDRESS, 8) }; - let length = u64::from_le_bytes([ - length[0], length[1], length[2], length[3], length[4], length[5], length[6], length[7], - ]) as usize; - unsafe { slice_from_raw_parts(CALLDATA_ADDRESS + 8, length) } -} - -pub fn log(data_ptr: u64, data_size: u64, topics_ptr: u64, topics_size: u64) { - unsafe { - asm!( - "ecall", - in("a0") data_ptr, - in("a1") data_size, - in("a2") topics_ptr, - in("a3") topics_size, - in("t0") u8::from(Syscall::Log) - ); - } -} - -#[allow(non_snake_case)] -#[no_mangle] -fn DefaultHandler() { - revert(); -} - -#[allow(non_snake_case)] -#[no_mangle] -fn ExceptionHandler(_trap_frame: &riscv_rt::TrapFrame) -> ! { - revert(); -} +# Test -pub fn call_contract(addr: Address, value: u64, data: &[u8], ret_size: u64) -> Option { - let mut ret_data = Vec::with_capacity(ret_size as usize); +The [R55](https://github.com/r55-eth/r55/tree/main/r55) crate has an [e2e test](https://github.com/r55-eth/r55/tree/main/r55/tests/e2e.rs) +that puts everything together in an end-to-end PoC, compiling the +[erc20](https://github.com/r55-eth/r55/tree/main/erc20) contract, deploying +it to an internal instance of [revm-r55](https://github.com/r0qs/revm), and +running two transactions on it, first a `mint` then a `balance_of` check. - call( - addr, - value, - data.as_ptr() as u64, - data.len() as u64, - ret_data.as_ptr() as u64, - ret_size, - ); +You'll need to install Rust's RISCV toolchain: - Some(Bytes::from(ret_data)) -} +```console +$ rustup install nightly-2024-02-01-x86_64-unknown-linux-gnu ``` -And finally, where the magic happens is in the r55 crate, where we merge the riscv runtime with revm: -```r55/src/exec.rs -use alloy_core::primitives::Keccak256; -use core::{cell::RefCell, ops::Range}; -use eth_riscv_interpreter::setup_from_elf; -use eth_riscv_syscalls::Syscall; -use revm::{ - handler::register::EvmHandler, - interpreter::{ - CallInputs, CallScheme, CallValue, Host, InstructionResult, Interpreter, InterpreterAction, - InterpreterResult, SharedMemory, - }, - primitives::{address, Address, Bytes, ExecutionResult, Log, Output, TransactTo, B256, U256}, - Database, Evm, Frame, FrameOrResult, InMemoryDB, -}; -use rvemu::{emulator::Emulator, exception::Exception}; -use std::{collections::BTreeMap, rc::Rc, sync::Arc}; - -use super::error::{Error, Result, TxResult}; -use super::gas; -use super::syscall_gas; - -pub fn deploy_contract(db: &mut InMemoryDB, bytecode: Bytes) -> Result
{ - let mut evm = Evm::builder() - .with_db(db) - .modify_tx_env(|tx| { - tx.caller = address!("000000000000000000000000000000000000000A"); - tx.transact_to = TransactTo::Create; - tx.data = bytecode; - tx.value = U256::from(0); - }) - .append_handler_register(handle_register) - .build(); - evm.cfg_mut().limit_contract_code_size = Some(usize::MAX); - - let result = evm.transact_commit()?; - - match result { - ExecutionResult::Success { - output: Output::Create(_value, Some(addr)), - .. - } => { - println!("Deployed at addr: {:?}", addr); - Ok(addr) - } - result => Err(Error::UnexpectedExecResult(result)), - } -} - -pub fn run_tx(db: &mut InMemoryDB, addr: &Address, calldata: Vec) -> Result { - let mut evm = Evm::builder() - .with_db(db) - .modify_tx_env(|tx| { - tx.caller = address!("000000000000000000000000000000000000000A"); - tx.transact_to = TransactTo::Call(*addr); - tx.data = calldata.into(); - tx.value = U256::from(0); - tx.gas_price = U256::from(42); - tx.gas_limit = 100_000; - }) - .append_handler_register(handle_register) - .build(); - - let result = evm.transact_commit()?; - - match result { - ExecutionResult::Success { - reason: _, - gas_used, - gas_refunded: _, - logs, - output: Output::Call(value), - .. - } => { - println!("Tx result: {:?}", value); - Ok(TxResult { - output: value.into(), - logs, - gas_used, - status: true, - }) - } - result => Err(Error::UnexpectedExecResult(result)), - } -} - -#[derive(Debug)] -struct RVEmu { - emu: Emulator, - returned_data_destiny: Option>, -} - -fn riscv_context(frame: &Frame) -> Option { - let interpreter = frame.interpreter(); - - println!("Creating RISC-V context:"); - println!("Contract address: {}", interpreter.contract.target_address); - println!("Input size: {}", interpreter.contract.input.len()); - - let Some((0xFF, bytecode)) = interpreter.bytecode.split_first() else { - return None; - }; - println!("RISC-V bytecode size: {}", bytecode.len()); - - match setup_from_elf(bytecode, &interpreter.contract.input) { - Ok(emu) => { - println!( - "RISC-V emulator setup successfully with entry point: 0x{:x}", - emu.cpu.pc - ); - Some(RVEmu { - emu, - returned_data_destiny: None, - }) - } - Err(err) => { - println!("Failed to setup from ELF: {err}"); - None - } - } -} - -pub fn handle_register(handler: &mut EvmHandler<'_, EXT, DB>) { - let call_stack = Rc::>>::new(RefCell::new(Vec::new())); - - // create a riscv context on call frame. - let call_stack_inner = call_stack.clone(); - let old_handle = handler.execution.call.clone(); - handler.execution.call = Arc::new(move |ctx, inputs| { - let result = old_handle(ctx, inputs); - if let Ok(FrameOrResult::Frame(frame)) = &result { - println!("----"); - println!("Frame created successfully"); - println!("Contract: {}", frame.interpreter().contract.target_address); - println!("Code size: {}", frame.interpreter().bytecode.len()); - - let context = riscv_context(frame); - println!("RISC-V context created: {}", context.is_some()); - println!("----"); - - call_stack_inner.borrow_mut().push(context); - } - result - }); +Now run: - // create a riscv context on create frame. - let call_stack_inner = call_stack.clone(); - let old_handle = handler.execution.create.clone(); - handler.execution.create = Arc::new(move |ctx, inputs| { - let result = old_handle(ctx, inputs); - if let Ok(FrameOrResult::Frame(frame)) = &result { - call_stack_inner.borrow_mut().push(riscv_context(frame)); - } - result - }); +```console +$ cargo test --package r55 --test e2e -- erc20 --exact --show-output +... +Compiling deploy: erc20 +Cargo command completed successfully +Deployed at addr: 0x522b3294e6d06aa25ad0f1b8891242e335d3b459 +Tx result: 0x +Tx result: 0x000000000000000000000000000000000000000000000000000000000000002a +``` - // execute riscv context or old logic. - let old_handle = handler.execution.execute_frame.clone(); - handler.execution.execute_frame = Arc::new(move |frame, memory, instraction_table, ctx| { - let depth = call_stack.borrow().len() - 1; - let result = if let Some(Some(riscv_context)) = call_stack.borrow_mut().last_mut() { - println!( - "\n*[{}] RISC-V Emu Handler (with PC: 0x{:x})", - depth, riscv_context.emu.cpu.pc - ); +First R55 compiles the runtime RISCV-ELF binary that will be deployed. This is +needed to also compile the initcode RISCV-ELF binary that runs the constructor +and creates the contract. +The `mint` function has no return values, seen in `Tx result: 0x`. We minted 42 +tokens to our test account in the first transaction, and we can see in the +second transaction that indeed the balance is 42 (0x2a). - execute_riscv(riscv_context, frame.interpreter_mut(), memory, ctx)? - } else { - println!("\n*[OLD Handler]"); - old_handle(frame, memory, instraction_table, ctx)? - }; +# Architecture - // if it is return pop the stack. - if result.is_return() { - println!("=== RETURN Frame ==="); - call_stack.borrow_mut().pop(); - println!( - "Popped frame from stack. Remaining frames: {}", - call_stack.borrow().len() - ); +The compiler uses `rustc`, `llvm`, +[eth-riscv-syscalls](https://github.com/r55-eth/r55/tree/main/eth-riscv-syscalls), +[eth-riscv-runtime](https://github.com/r55-eth/r55/tree/main/eth-riscv-runtime) +and [riscv-rt](https://github.com/rust-embedded/riscv/tree/master/riscv-rt) to +compile and link ELF binaries with low-level syscalls to be executed by +[rvemu-r55](https://github.com/r55-eth/rvemu): - // if cross-contract call, copy return data into memory range expected by the parent - if !call_stack.borrow().is_empty() { - if let Some(Some(parent)) = call_stack.borrow_mut().last_mut() { - if let Some(return_range) = &parent.returned_data_destiny { - if let InterpreterAction::Return { result: res } = &result { - // Get allocated memory slice - let return_memory = parent - .emu - .cpu - .bus - .get_dram_slice(return_range.clone()) - .expect("unable to get memory from return range"); +```mermaid +graph TD; + RustContract[Rust contract] --> CompiledContract[compiled contract] + rustc --> CompiledContract + llvm --> CompiledContract + EthRiscVSyscalls[eth-riscv-syscalls] --> CompiledContract + EthRiscVRuntime1[eth-riscv-runtime] --> CompiledContract + CompiledContract --> LinkedRuntimeBytecode[linked runtime bytecode] + EthRiscVRuntime2[eth-riscv-runtime] --> LinkedRuntimeBytecode + riscv_rt[riscv-rt] --> LinkedRuntimeBytecode + LinkedRuntimeBytecode --> LinkedInitBytecode[linked init bytecode] + EthRiscVRuntime3[eth-riscv-runtime] --> LinkedInitBytecode +``` - println!("Return data: {:?}", res.output); - println!("Memory range: {:?}", return_range); - println!("Memory size: {}", return_memory.len()); +The execution environment depends on [revm](https://github.com/bluealloy/revm), +and relies on the [rvemu-r55](https://github.com/r55-eth/rvemu) RISCV +interpreter and +[eth-riscv-runtime](https://github.com/r55-eth/r55/tree/main/eth-riscv-runtime). - // Write return data to parent's memory - if res.output.len() == return_memory.len() { - println!("Copying output to memory"); - return_memory.copy_from_slice(&res.output); - } - } - } - } - } - } - - Ok(result) - }); -} - -fn execute_riscv( - rvemu: &mut RVEmu, - interpreter: &mut Interpreter, - shared_memory: &mut SharedMemory, - host: &mut dyn Host, -) -> Result { - println!( - "{} RISC-V execution:\n PC: {:#x}\n Contract: {}\n Return data dst: {:#?}", - if rvemu.emu.cpu.pc == 0x80300000 { - "Starting" - } else { - "Resuming" - }, - rvemu.emu.cpu.pc, - interpreter.contract.target_address, - &rvemu.returned_data_destiny - ); - - let emu = &mut rvemu.emu; - emu.cpu.is_count = true; - - let returned_data_destiny = &mut rvemu.returned_data_destiny; - if let Some(destiny) = std::mem::take(returned_data_destiny) { - let data = emu.cpu.bus.get_dram_slice(destiny)?; - if shared_memory.len() >= data.len() { - data.copy_from_slice(shared_memory.slice(0, data.len())); - } - println!("Loaded return data: {}", Bytes::copy_from_slice(data)); - } - - let return_revert = |interpreter: &mut Interpreter, gas_used: u64| { - let _ = interpreter.gas.record_cost(gas_used); - Ok(InterpreterAction::Return { - result: InterpreterResult { - result: InstructionResult::Revert, - // return empty bytecode - output: Bytes::new(), - gas: interpreter.gas, - }, - }) - }; - - // Run emulator and capture ecalls - loop { - let run_result = emu.start(); - match run_result { - Err(Exception::EnvironmentCallFromMMode) => { - let t0: u64 = emu.cpu.xregs.read(5); - - let Ok(syscall) = Syscall::try_from(t0 as u8) else { - println!("Unhandled syscall: {:?}", t0); - return return_revert(interpreter, interpreter.gas.spent()); - }; - println!("> [Syscall::{} - {:#04x}]", syscall, t0); - - match syscall { - Syscall::Return => { - let ret_offset: u64 = emu.cpu.xregs.read(10); - let ret_size: u64 = emu.cpu.xregs.read(11); - - let r55_gas = r55_gas_used(&emu.cpu.inst_counter); - println!("> total R55 gas: {}", r55_gas); - - // RETURN logs the gas of the whole risc-v instruction set - syscall_gas!(interpreter, r55_gas); - - let data_bytes = dram_slice(emu, ret_offset, ret_size)?; - - println!("interpreter remaining gas: {}", interpreter.gas.remaining()); - return Ok(InterpreterAction::Return { - result: InterpreterResult { - result: InstructionResult::Return, - output: data_bytes.to_vec().into(), - gas: interpreter.gas, // FIXME: gas is not correct - }, - }); - } - Syscall::SLoad => { - let key: u64 = emu.cpu.xregs.read(10); - println!( - "SLOAD ({}) - Key: {}", - interpreter.contract.target_address, key - ); - match host.sload(interpreter.contract.target_address, U256::from(key)) { - Some((value, is_cold)) => { - println!( - "SLOAD ({}) - Value: {}", - interpreter.contract.target_address, value - ); - let limbs = value.as_limbs(); - emu.cpu.xregs.write(10, limbs[0]); - emu.cpu.xregs.write(11, limbs[1]); - emu.cpu.xregs.write(12, limbs[2]); - emu.cpu.xregs.write(13, limbs[3]); - syscall_gas!( - interpreter, - if is_cold { - gas::SLOAD_COLD - } else { - gas::SLOAD_WARM - } - ); - } - _ => { - return return_revert(interpreter, interpreter.gas.spent()); - } - } - } - Syscall::SStore => { - let key: u64 = emu.cpu.xregs.read(10); - let first: u64 = emu.cpu.xregs.read(11); - let second: u64 = emu.cpu.xregs.read(12); - let third: u64 = emu.cpu.xregs.read(13); - let fourth: u64 = emu.cpu.xregs.read(14); - let result = host.sstore( - interpreter.contract.target_address, - U256::from(key), - U256::from_limbs([first, second, third, fourth]), - ); - if let Some(result) = result { - syscall_gas!( - interpreter, - if result.is_cold { - gas::SSTORE_COLD - } else { - gas::SSTORE_WARM - } - ); - } - } - Syscall::Call => { - let a0: u64 = emu.cpu.xregs.read(10); - let a1: u64 = emu.cpu.xregs.read(11); - let a2: u64 = emu.cpu.xregs.read(12); - let addr = Address::from_word(U256::from_limbs([a0, a1, a2, 0]).into()); - let value: u64 = emu.cpu.xregs.read(13); - - // Get calldata - let args_offset: u64 = emu.cpu.xregs.read(14); - let args_size: u64 = emu.cpu.xregs.read(15); - let calldata: Bytes = emu - .cpu - .bus - .get_dram_slice(args_offset..(args_offset + args_size)) - .unwrap_or(&mut []) - .to_vec() - .into(); - - // Store where return data should go - let ret_offset = emu.cpu.xregs.read(16); - let ret_size = emu.cpu.xregs.read(17); - println!( - "Return data will be written to: {}..{}", - ret_offset, - ret_offset + ret_size - ); - - // Initialize memory region for return data - let return_memory = emu - .cpu - .bus - .get_dram_slice(ret_offset..(ret_offset + ret_size))?; - return_memory.fill(0); - rvemu.returned_data_destiny = Some(ret_offset..(ret_offset + ret_size)); - - // Calculate gas for the call - // TODO: Check correctness (tried using evm.codes as ref but i'm no gas wizard) - let (empty_account_cost, addr_access_cost) = match host.load_account(addr) { - Some(account) => { - if account.is_cold { - (0, gas::CALL_NEW_ACCOUNT) - } else { - (0, gas::CALL_BASE) - } - } - None => (gas::CALL_EMPTY_ACCOUNT, gas::CALL_NEW_ACCOUNT), - }; - let value_cost = if value != 0 { gas::CALL_VALUE } else { 0 }; - let call_gas_cost = empty_account_cost + addr_access_cost + value_cost; - syscall_gas!(interpreter, call_gas_cost); - - println!("Call context:"); - println!(" Caller: {}", interpreter.contract.target_address); - println!(" Target Address: {}", addr); - println!(" Value: {}", value); - println!(" Calldata: {:?}", calldata); - return Ok(InterpreterAction::Call { - inputs: Box::new(CallInputs { - input: calldata, - gas_limit: interpreter.gas.remaining(), - target_address: addr, - bytecode_address: addr, - caller: interpreter.contract.target_address, - value: CallValue::Transfer(U256::from(value)), - scheme: CallScheme::Call, - is_static: false, - is_eof: false, - return_memory_offset: 0..0, // handled with `returned_data_destiny` - }), - }); - } - Syscall::Revert => { - return Ok(InterpreterAction::Return { - result: InterpreterResult { - result: InstructionResult::Revert, - output: Bytes::from(0u32.to_le_bytes()), //TODO: return revert(0,0) - gas: interpreter.gas, // FIXME: gas is not correct - }, - }); - } - Syscall::Caller => { - let caller = interpreter.contract.caller; - // Break address into 3 u64s and write to registers - let caller_bytes = caller.as_slice(); - let first_u64 = u64::from_be_bytes(caller_bytes[0..8].try_into()?); - emu.cpu.xregs.write(10, first_u64); - let second_u64 = u64::from_be_bytes(caller_bytes[8..16].try_into()?); - emu.cpu.xregs.write(11, second_u64); - let mut padded_bytes = [0u8; 8]; - padded_bytes[..4].copy_from_slice(&caller_bytes[16..20]); - let third_u64 = u64::from_be_bytes(padded_bytes); - emu.cpu.xregs.write(12, third_u64); - } - Syscall::Keccak256 => { - let ret_offset: u64 = emu.cpu.xregs.read(10); - let ret_size: u64 = emu.cpu.xregs.read(11); - let data_bytes = dram_slice(emu, ret_offset, ret_size)?; - - let mut hasher = Keccak256::new(); - hasher.update(data_bytes); - let hash: [u8; 32] = hasher.finalize().into(); - - // Write the hash to the emulator's registers - emu.cpu - .xregs - .write(10, u64::from_le_bytes(hash[0..8].try_into()?)); - emu.cpu - .xregs - .write(11, u64::from_le_bytes(hash[8..16].try_into()?)); - emu.cpu - .xregs - .write(12, u64::from_le_bytes(hash[16..24].try_into()?)); - emu.cpu - .xregs - .write(13, u64::from_le_bytes(hash[24..32].try_into()?)); - } - Syscall::CallValue => { - let value = interpreter.contract.call_value; - let limbs = value.into_limbs(); - emu.cpu.xregs.write(10, limbs[0]); - emu.cpu.xregs.write(11, limbs[1]); - emu.cpu.xregs.write(12, limbs[2]); - emu.cpu.xregs.write(13, limbs[3]); - } - Syscall::BaseFee => { - let value = host.env().block.basefee; - let limbs = value.as_limbs(); - emu.cpu.xregs.write(10, limbs[0]); - emu.cpu.xregs.write(11, limbs[1]); - emu.cpu.xregs.write(12, limbs[2]); - emu.cpu.xregs.write(13, limbs[3]); - } - Syscall::ChainId => { - let value = host.env().cfg.chain_id; - emu.cpu.xregs.write(10, value); - } - Syscall::GasLimit => { - let limit = host.env().block.gas_limit; - let limbs = limit.as_limbs(); - emu.cpu.xregs.write(10, limbs[0]); - emu.cpu.xregs.write(11, limbs[1]); - emu.cpu.xregs.write(12, limbs[2]); - emu.cpu.xregs.write(13, limbs[3]); - } - Syscall::Number => { - let number = host.env().block.number; - let limbs = number.as_limbs(); - emu.cpu.xregs.write(10, limbs[0]); - emu.cpu.xregs.write(11, limbs[1]); - emu.cpu.xregs.write(12, limbs[2]); - emu.cpu.xregs.write(13, limbs[3]); - } - Syscall::Timestamp => { - let timestamp = host.env().block.timestamp; - let limbs = timestamp.as_limbs(); - emu.cpu.xregs.write(10, limbs[0]); - emu.cpu.xregs.write(11, limbs[1]); - emu.cpu.xregs.write(12, limbs[2]); - emu.cpu.xregs.write(13, limbs[3]); - } - Syscall::GasPrice => { - let value = host.env().tx.gas_price; - let limbs = value.as_limbs(); - emu.cpu.xregs.write(10, limbs[0]); - emu.cpu.xregs.write(11, limbs[1]); - emu.cpu.xregs.write(12, limbs[2]); - emu.cpu.xregs.write(13, limbs[3]); - } - Syscall::Origin => { - // Syscall::Origin - let origin = host.env().tx.caller; - // Break address into 3 u64s and write to registers - let origin_bytes = origin.as_slice(); - - let first_u64 = u64::from_be_bytes(origin_bytes[0..8].try_into().unwrap()); - emu.cpu.xregs.write(10, first_u64); - - let second_u64 = - u64::from_be_bytes(origin_bytes[8..16].try_into().unwrap()); - emu.cpu.xregs.write(11, second_u64); - - let mut padded_bytes = [0u8; 8]; - padded_bytes[..4].copy_from_slice(&origin_bytes[16..20]); - let third_u64 = u64::from_be_bytes(padded_bytes); - emu.cpu.xregs.write(12, third_u64); - } - Syscall::Log => { - let data_ptr: u64 = emu.cpu.xregs.read(10); - let data_size: u64 = emu.cpu.xregs.read(11); - let topics_ptr: u64 = emu.cpu.xregs.read(12); - let topics_size: u64 = emu.cpu.xregs.read(13); - - // Read data - let data_slice = emu - .cpu - .bus - .get_dram_slice(data_ptr..(data_ptr + data_size)) - .unwrap_or(&mut []); - let data = data_slice.to_vec(); - - // Read topics - let topics_start = topics_ptr; - let topics_end = topics_ptr + topics_size * 32; - let topics_slice = emu - .cpu - .bus - .get_dram_slice(topics_start..topics_end) - .unwrap_or(&mut []); - let topics = topics_slice - .chunks(32) - .map(B256::from_slice) - .collect::>(); - - host.log(Log::new_unchecked( - interpreter.contract.target_address, - topics, - data.into(), - )); - } - } - } - Ok(_) => { - println!("Successful instruction at PC: {:#x}", emu.cpu.pc); - continue; - } - Err(e) => { - println!("Execution error: {:#?}", e); - syscall_gas!(interpreter, r55_gas_used(&emu.cpu.inst_counter)); - return return_revert(interpreter, interpreter.gas.spent()); - } - } - } -} - -/// Returns RISC-V DRAM slice in a given size range, starts with a given offset -fn dram_slice(emu: &mut Emulator, ret_offset: u64, ret_size: u64) -> Result<&mut [u8]> { - if ret_size != 0 { - Ok(emu - .cpu - .bus - .get_dram_slice(ret_offset..(ret_offset + ret_size))?) - } else { - Ok(&mut []) - } -} - -fn r55_gas_used(inst_count: &BTreeMap) -> u64 { - let total_cost = inst_count - .iter() - .map(|(inst_name, count)| - // Gas cost = number of instructions * cycles per instruction - match inst_name.as_str() { - // Gas map to approximate cost of each instruction - // References: - // http://ithare.com/infographics-operation-costs-in-cpu-clock-cycles/ - // https://www.evm.codes/?fork=cancun#54 - // Division and remainder - s if s.starts_with("div") || s.starts_with("rem") => count * 25, - // Multiplications - s if s.starts_with("mul") => count * 5, - // Loads - "lb" | "lh" | "lw" | "ld" | "lbu" | "lhu" | "lwu" => count * 3, // Cost analagous to `MLOAD` - // Stores - "sb" | "sh" | "sw" | "sd" | "sc.w" | "sc.d" => count * 3, // Cost analagous to `MSTORE` - // Branching - "beq" | "bne" | "blt" | "bge" | "bltu" | "bgeu" | "jal" | "jalr" => count * 3, - _ => *count, // All other instructions including `add` and `sub` - }) - .sum::(); - - // This is the minimum 'gas used' to ABI decode 'empty' calldata into Rust type arguments. Real calldata will take more gas. - // Internalising this would focus gas metering more on the function logic - let abi_decode_cost = 9_175_538; - - total_cost - abi_decode_cost -} - -``` -and here the gas helpers used in exec.rs: -```r55/src/gas.rs -// Standard EVM operation costs -pub const SLOAD_COLD: u64 = 2100; -pub const SLOAD_WARM: u64 = 100; -pub const SSTORE_COLD: u64 = 2200; -pub const SSTORE_WARM: u64 = 100; - -// Call-related costs -pub const CALL_EMPTY_ACCOUNT: u64 = 25000; -pub const CALL_NEW_ACCOUNT: u64 = 2600; -pub const CALL_VALUE: u64 = 9000; -pub const CALL_BASE: u64 = 100; - -// Macro to handle gas accounting for syscalls. -// Returns OutOfGas InterpreterResult if gas limit is exceeded. -#[macro_export] -macro_rules! syscall_gas { - ($interpreter:expr, $gas_cost:expr $(,)?) => {{ - let remaining_before = $interpreter.gas.remaining(); - let gas_cost = $gas_cost; - - println!("> About to log gas costs:"); - println!(" - Operation cost: {}", gas_cost); - println!(" - Gas remaining: {}", remaining_before); - println!(" - Gas limit: {}", $interpreter.gas.limit()); - println!(" - Gas spent: {}", $interpreter.gas.spent()); - - if !$interpreter.gas.record_cost(gas_cost) { - eprintln!("OUT OF GAS"); - return Ok(InterpreterAction::Return { - result: InterpreterResult { - result: InstructionResult::OutOfGas, - output: Bytes::new(), - gas: $interpreter.gas, - }, - }); - } - - println!("> Gas recorded successfully:"); - println!(" - Gas remaining: {}", remaining_before); - println!(" - Gas spent: {}", $interpreter.gas.spent()); - }}; -} -``` - -as you can see, it is important to understand how revm works first, so here you have some context: -"""md - -# Evm Builder - -The builder creates or modifies the EVM and applies different handlers. -It allows setting external context and registering handler custom logic. - -The revm `Evm` consists of `Context` and `Handler`. -`Context` is additionally split between `EvmContext` (contains generic `Database`) and `External` context (generic without restrain). -Read [evm](./evm.md) for more information on the internals. - -The `Builder` ties dependencies between generic `Database`, `External` context and `Spec`. -It allows handle registers to be added that implement logic on those generics. -As they are interconnected, setting `Database` or `ExternalContext` resets handle registers, so builder stages are introduced to mitigate those misuses. - -Simple example of using `EvmBuilder`: - -```rust,ignore - use crate::evm::Evm; - - // build Evm with default values. - let mut evm = Evm::builder().build(); - let output = evm.transact(); -``` - -## Builder Stages - -There are two builder stages that are used to mitigate potential misuse of the builder: - * `SetGenericStage`: Initial stage that allows setting the database and external context. - * `HandlerStage`: Allows setting the handler registers but is explicit about setting new generic type as it will void the handler registers. - -Functions from one stage are just renamed functions from other stage, it is made so that user is more aware of what underlying function does. -For example, in `SettingDbStage` we have `with_db` function while in `HandlerStage` we have `reset_handler_with_db`, both of them set the database but the latter also resets the handler. -There are multiple functions that are common to both stages such as `build`. - -### Builder naming conventions -In both stages we have: - * `build` creates the Evm. - * `spec_id` creates new mainnet handler and reapplies all the handler registers. - * `modify_*` functions are used to modify the database, external context or Env. - * `clear_*` functions allows setting default values for Environment. - * `append_handler_register_*` functions are used to push handler registers. - This will transition the builder to the `HandlerStage`. - -In `SetGenericStage` we have: - * `with_*` are found in `SetGenericStage` and are used to set the generics. - -In `HandlerStage` we have: - * `reset_handler_with_*` is used if we want to change some of the generic types this will reset the handler registers. - This will transition the builder to the `SetGenericStage`. - -# Creating and modification of Evm - -Evm implements functions that allow using the `EvmBuilder` without even knowing that it exists. -The most obvious one is `Evm::builder()` that creates a new builder with default values. - -Additionally, a function that is very important is `evm.modify()` that allows modifying the Evm. -It returns a builder, allowing users to modify the Evm. - -# Examples -The following example uses the builder to create an `Evm` with inspector: -```rust,ignore - use crate::{ - db::EmptyDB, Context, EvmContext, inspector::inspector_handle_register, inspectors::NoOpInspector, Evm, - }; - - // Create the evm. - let evm = Evm::builder() - .with_db(EmptyDB::default()) - .with_external_context(NoOpInspector) - // Register will modify Handler and call NoOpInspector. - .append_handler_register(inspector_handle_register) - // .with_db(..) does not compile as we already locked the builder generics, - // alternative fn is reset_handler_with_db(..) - .build(); - - // Execute the evm. - let output = evm.transact(); - - // Extract evm context. - let Context { - external, - evm: EvmContext { db, .. }, - } = evm.into_context(); -``` - -The next example changes the spec id and environment of an already built evm. -```rust,ignore - use crate::{Evm,SpecId::BERLIN}; - - // Create default evm. - let evm = Evm::builder().build(); - - // Modify evm spec. - let evm = evm.modify().with_spec_id(BERLIN).build(); - - // Shortcut for above. - let mut evm = evm.modify_spec_id(BERLIN); - - // Execute the evm. - let output1 = evm.transact(); - - // Example of modifying the tx env. - let mut evm = evm.modify().modify_tx_env(|env| env.gas_price = 0.into()).build(); - - // Execute the evm with modified tx env. - let output2 = evm.transact(); -``` - -Example of adding custom precompiles to Evm. - -```rust,ignore -use super::SpecId; -use crate::{ - db::EmptyDB, - inspector::inspector_handle_register, - inspectors::NoOpInspector, - primitives::{Address, Bytes, ContextStatefulPrecompile, ContextPrecompile, PrecompileResult}, - Context, Evm, EvmContext, -}; -use std::sync::Arc; - -struct CustomPrecompile; - -impl ContextStatefulPrecompile, ()> for CustomPrecompile { - fn call( - &self, - _input: &Bytes, - _gas_limit: u64, - _context: &mut EvmContext, - _extcontext: &mut (), - ) -> PrecompileResult { - Ok((10, Bytes::new())) - } -} -fn main() { - let mut evm = Evm::builder() - .with_empty_db() - .with_spec_id(SpecId::HOMESTEAD) - .append_handler_register(|handler| { - let precompiles = handler.pre_execution.load_precompiles(); - handler.pre_execution.load_precompiles = Arc::new(move || { - let mut precompiles = precompiles.clone(); - precompiles.extend([( - Address::ZERO, - ContextPrecompile::ContextStateful(Arc::new(CustomPrecompile)), - )]); - precompiles - }); - }) - .build(); - - evm.transact().unwrap(); -} - -``` - -## Appending handler registers - -Handler registers are simple functions that allow modifying the `Handler` logic by replacing the handler functions. -They are used to add custom logic to the evm execution but as they are free to modify the `Handler` in any form they want. -There may be conflicts if handlers that override the same function are added. - -The most common use case for adding new logic to `Handler` is `Inspector` that is used to inspect the execution of the evm. -Example of this can be found in [`Inspector`](./inspector.md) documentation. -""" - -as you can see, `r55/src/exec.rs` impls `handle_register()` to use the risc-v instruction set. -it is also worth mentioning that (inside the `handle_register() fn`) i added this code to try to handle x-contract calls: -``` - // if cross-contract call, copy return data into memory range expected by the parent - if !call_stack.borrow().is_empty() { - if let Some(Some(parent)) = call_stack.borrow_mut().last_mut() { - if let Some(return_range) = &parent.returned_data_destiny { - if let InterpreterAction::Return { result: res } = &result { - // Get allocated memory slice - let return_memory = parent - .emu - .cpu - .bus - .get_dram_slice(return_range.clone()) - .expect("unable to get memory from return range"); - - println!("Return data: {:?}", res.output); - println!("Memory range: {:?}", return_range); - println!("Memory size: {}", return_memory.len()); - - // Write return data to parent's memory - if res.output.len() == return_memory.len() { - println!("Copying output to memory"); - return_memory.copy_from_slice(&res.output); - } - } - } - } - } -``` - -with all of that, you should have a good understanding of the project and what i'm trying to achieve. -so to finish, let me share the test suit that i'm using: - -```r55/tests/e2e.rs -use alloy_primitives::Bytes; -use alloy_sol_types::SolValue; -use r55::{ - compile_deploy, compile_with_prefix, - exec::{deploy_contract, run_tx}, - test_utils::{add_balance_to_db, get_selector_from_sig, initialize_logger}, -}; -use revm::{ - primitives::{address, Address}, - InMemoryDB, -}; - -const ERC20_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../erc20"); - -#[test] -fn erc20() { - initialize_logger(); - - let mut db = InMemoryDB::default(); - - let bytecode = compile_with_prefix(compile_deploy, ERC20_PATH).unwrap(); - let addr1 = deploy_contract(&mut db, bytecode.clone()).unwrap(); - let addr2 = deploy_contract(&mut db, bytecode).unwrap(); - - let selector_balance = get_selector_from_sig("balance_of"); - let selector_x_balance = get_selector_from_sig("x_balance_of"); - let selector_mint = get_selector_from_sig("mint"); - let alice: Address = address!("000000000000000000000000000000000000000A"); - let value_mint: u64 = 42; - let mut calldata_balance = alice.abi_encode(); - let mut calldata_mint = (alice, value_mint).abi_encode(); - let mut calldata_x_balance = (alice, addr1).abi_encode(); - - add_balance_to_db(&mut db, alice, 1e18 as u64); - - let mut complete_calldata_balance = selector_balance.to_vec(); - complete_calldata_balance.append(&mut calldata_balance); - - let mut complete_calldata_mint = selector_mint.to_vec(); - complete_calldata_mint.append(&mut calldata_mint); - - let mut complete_calldata_x_balance = selector_x_balance.to_vec(); - complete_calldata_x_balance.append(&mut calldata_x_balance); - - println!("\n----------------------------------------------------------"); - println!("-- MINT TX -----------------------------------------------"); - println!("----------------------------------------------------------"); - println!( - " > TX Calldata: {:#?\n}", - Bytes::from(complete_calldata_mint.clone()) - ); - run_tx(&mut db, &addr1, complete_calldata_mint.clone()).unwrap(); - println!("\n----------------------------------------------------------"); - println!("-- BALANCE OF TX -----------------------------------------"); - println!("----------------------------------------------------------"); - println!( - " > TX Calldata: {:#?}\n", - Bytes::from(complete_calldata_balance.clone()) - ); - run_tx(&mut db, &addr1, complete_calldata_balance.clone()).unwrap(); - println!("\n----------------------------------------------------------"); - println!("-- X-CONTRACT BALANCE OF TX ------------------------------"); - println!("----------------------------------------------------------"); - println!( - " > TX Calldata: {:#?}\n", - Bytes::from(complete_calldata_x_balance.clone()) - ); - match run_tx(&mut db, &addr2, complete_calldata_x_balance.clone()) { - Ok(res) => info!("res: {:#?}", res), - Err(e) => error!("{:#?}", e), - }; -} -``` - -and the logs that they output: -``` ----- erc20 stdout ---- -Creating RISC-V context: -Contract address: 0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d -Input size: 0 -RISC-V bytecode size: 96233 -RISC-V emulator setup successfully with entry point: 0x80300000 - -*[0] RISC-V Emu Handler (with PC: 0x80300000) -Starting RISC-V execution: - PC: 0x80300000 - Contract: 0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d - Return data dst: None -> [Syscall::return - 0xf3] -> total R55 gas: 131383 -> About to log gas costs: - - Operation cost: 131383 - - Gas remaining: 18446744073708358085 - - Gas limit: 18446744073708358085 - - Gas spent: 0 -> Gas recorded successfully: - - Gas remaining: 18446744073708358085 - - Gas spent: 131383 -interpreter remaining gas: 18446744073708226702 -=== RETURN Frame === -Popped frame from stack. Remaining frames: 0 -Deployed at addr: 0xf6a171f57acac30c292e223ea8adbb28abd3e14d -Creating RISC-V context: -Contract address: 0x114c28ea2CDe99e41D2566B1CfAA64a84f564caf -Input size: 0 -RISC-V bytecode size: 96233 -RISC-V emulator setup successfully with entry point: 0x80300000 - -*[0] RISC-V Emu Handler (with PC: 0x80300000) -Starting RISC-V execution: - PC: 0x80300000 - Contract: 0x114c28ea2CDe99e41D2566B1CfAA64a84f564caf - Return data dst: None -> [Syscall::return - 0xf3] -> total R55 gas: 131383 -> About to log gas costs: - - Operation cost: 131383 - - Gas remaining: 18446744073708358085 - - Gas limit: 18446744073708358085 - - Gas spent: 0 -> Gas recorded successfully: - - Gas remaining: 18446744073708358085 - - Gas spent: 131383 -interpreter remaining gas: 18446744073708226702 -=== RETURN Frame === -Popped frame from stack. Remaining frames: 0 -Deployed at addr: 0x114c28ea2cde99e41d2566b1cfaa64a84f564caf - ----------------------------------------------------------- --- MINT TX ----------------------------------------------- ----------------------------------------------------------- - > TX Calldata: 0xdaf0b3c5000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000002a ----- -Frame created successfully -Contract: 0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d -Code size: 80970 -Creating RISC-V context: -Contract address: 0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d -Input size: 68 -RISC-V bytecode size: 80969 -RISC-V emulator setup successfully with entry point: 0x80300000 -RISC-V context created: true ----- - -*[0] RISC-V Emu Handler (with PC: 0x80300000) -Starting RISC-V execution: - PC: 0x80300000 - Contract: 0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d - Return data dst: None -> [Syscall::caller - 0x33] -> [Syscall::keccak256 - 0x20] -> [Syscall::sload - 0x54] -SLOAD (0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d) - Key: 2674963704631452285 -SLOAD (0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d) - Value: 0 -> About to log gas costs: - - Operation cost: 2100 - - Gas remaining: 78656 - - Gas limit: 78656 - - Gas spent: 0 -> Gas recorded successfully: - - Gas remaining: 78656 - - Gas spent: 2100 -> [Syscall::keccak256 - 0x20] -> [Syscall::sstore - 0x55] -> About to log gas costs: - - Operation cost: 100 - - Gas remaining: 76556 - - Gas limit: 78656 - - Gas spent: 2100 -> Gas recorded successfully: - - Gas remaining: 76556 - - Gas spent: 2200 -> [Syscall::log - 0xa0] -> [Syscall::return - 0xf3] -> total R55 gas: 24041 -> About to log gas costs: - - Operation cost: 24041 - - Gas remaining: 76456 - - Gas limit: 78656 - - Gas spent: 2200 -> Gas recorded successfully: - - Gas remaining: 76456 - - Gas spent: 26241 -interpreter remaining gas: 52415 -=== RETURN Frame === -Popped frame from stack. Remaining frames: 0 -Tx result: 0x0000000000000000000000000000000000000000000000000000000000000001 - ----------------------------------------------------------- --- BALANCE OF TX ----------------------------------------- ----------------------------------------------------------- - > TX Calldata: 0xd35a73cd000000000000000000000000000000000000000000000000000000000000000a - ----- -Frame created successfully -Contract: 0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d -Code size: 80970 -Creating RISC-V context: -Contract address: 0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d -Input size: 36 -RISC-V bytecode size: 80969 -RISC-V emulator setup successfully with entry point: 0x80300000 -RISC-V context created: true ----- - -*[0] RISC-V Emu Handler (with PC: 0x80300000) -Starting RISC-V execution: - PC: 0x80300000 - Contract: 0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d - Return data dst: None -> [Syscall::callvalue - 0x34] -> [Syscall::keccak256 - 0x20] -> [Syscall::sload - 0x54] -SLOAD (0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d) - Key: 2674963704631452285 -SLOAD (0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d) - Value: 42 -> About to log gas costs: - - Operation cost: 2100 - - Gas remaining: 78796 - - Gas limit: 78796 - - Gas spent: 0 -> Gas recorded successfully: - - Gas remaining: 78796 - - Gas spent: 2100 -> [Syscall::return - 0xf3] -> total R55 gas: 4018 -> About to log gas costs: - - Operation cost: 4018 - - Gas remaining: 76696 - - Gas limit: 78796 - - Gas spent: 2100 -> Gas recorded successfully: - - Gas remaining: 76696 - - Gas spent: 6118 -interpreter remaining gas: 72678 -=== RETURN Frame === -Popped frame from stack. Remaining frames: 0 -Tx result: 0x000000000000000000000000000000000000000000000000000000000000002a - ----------------------------------------------------------- --- X-CONTRACT BALANCE OF TX ------------------------------ ----------------------------------------------------------- - > TX Calldata: 0x51f59fb7000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000f6a171f57acac30c292e223ea8adbb28abd3e14d - ----- -Frame created successfully -Contract: 0x114c28ea2CDe99e41D2566B1CfAA64a84f564caf -Code size: 80970 -Creating RISC-V context: -Contract address: 0x114c28ea2CDe99e41D2566B1CfAA64a84f564caf -Input size: 68 -RISC-V bytecode size: 80969 -RISC-V emulator setup successfully with entry point: 0x80300000 -RISC-V context created: true ----- - -*[0] RISC-V Emu Handler (with PC: 0x80300000) -Starting RISC-V execution: - PC: 0x80300000 - Contract: 0x114c28ea2CDe99e41D2566B1CfAA64a84f564caf - Return data dst: None -> [Syscall::callvalue - 0x34] -> [Syscall::call - 0xf1] -Return data will be written to: 2150663548..2150663580 -> About to log gas costs: - - Operation cost: 2600 - - Gas remaining: 78428 - - Gas limit: 78428 - - Gas spent: 0 -> Gas recorded successfully: - - Gas remaining: 78428 - - Gas spent: 2600 -Call context: - Caller: 0x114c28ea2CDe99e41D2566B1CfAA64a84f564caf - Target Address: 0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d - Value: 0 - Calldata: 0xd35a73cd000000000000000000000000000000000000000000000000000000000000000a ----- -Frame created successfully -Contract: 0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d -Code size: 80970 -Creating RISC-V context: -Contract address: 0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d -Input size: 36 -RISC-V bytecode size: 80969 -RISC-V emulator setup successfully with entry point: 0x80300000 -RISC-V context created: true ----- - -*[1] RISC-V Emu Handler (with PC: 0x80300000) -Starting RISC-V execution: - PC: 0x80300000 - Contract: 0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d - Return data dst: None -> [Syscall::callvalue - 0x34] -> [Syscall::keccak256 - 0x20] -> [Syscall::sload - 0x54] -SLOAD (0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d) - Key: 2674963704631452285 -SLOAD (0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d) - Value: 42 -> About to log gas costs: - - Operation cost: 2100 - - Gas remaining: 75828 - - Gas limit: 75828 - - Gas spent: 0 -> Gas recorded successfully: - - Gas remaining: 75828 - - Gas spent: 2100 -> [Syscall::return - 0xf3] -> total R55 gas: 4018 -> About to log gas costs: - - Operation cost: 4018 - - Gas remaining: 73728 - - Gas limit: 75828 - - Gas spent: 2100 -> Gas recorded successfully: - - Gas remaining: 73728 - - Gas spent: 6118 -interpreter remaining gas: 69710 -=== RETURN Frame === -Popped frame from stack. Remaining frames: 1 -Return data: 0x000000000000000000000000000000000000000000000000000000000000002a -Memory range: 2150663548..2150663580 -Memory size: 32 -Copying output to memory - -*[0] RISC-V Emu Handler (with PC: 0x80300c5e) -Resuming RISC-V execution: - PC: 0x80300c5e - Contract: 0x114c28ea2CDe99e41D2566B1CfAA64a84f564caf - Return data dst: Some( - 2150663548..2150663580, -) -Loaded return data: 0x000000000000000000000000000000000000000000000000000000000000002a -> [Syscall::return - 0xf3] -> total R55 gas: 7553 -> About to log gas costs: - - Operation cost: 7553 - - Gas remaining: 145538 - - Gas limit: 78428 -thread 'erc20' panicked at /Users/rusowsky/.cargo/registry/src/index.crates.io-6f17d22bba15001f/revm-interpreter-5.0.0/src/gas.rs:66:9: -attempt to subtract with overflow -note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace -``` - -as u can see the cross-contract calls are almost ready! i am just facing some final issues with the gas calculations. To be more precise, the issue that i have is that the interpreter is refunding the whole unspent amount of each frame, making the "remaining" greater than the limit, which then creates an underflow. -i will share some of the relevant revm files to see if you can help me figure out how to fix the issue: - -> interpreter gas definition: in this case we want to look at `erase_cost()` fn -```revm/crates/interpreter/src/gas.rs -//! EVM gas calculation utilities. - -mod calc; -mod constants; - -pub use calc::*; -pub use constants::*; - -/// Represents the state of gas during execution. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Gas { - /// The initial gas limit. This is constant throughout execution. - limit: u64, - /// The remaining gas. - remaining: u64, - /// Refunded gas. This is used only at the end of execution. - refunded: i64, - /// Memoisation of values for memory expansion cost. - memory: MemoryGas, -} - -impl Gas { - /// Creates a new `Gas` struct with the given gas limit. - #[inline] - pub const fn new(limit: u64) -> Self { - Self { - limit, - remaining: limit, - refunded: 0, - memory: MemoryGas::new(), - } - } - - /// Creates a new `Gas` struct with the given gas limit, but without any gas remaining. - #[inline] - pub const fn new_spent(limit: u64) -> Self { - Self { - limit, - remaining: 0, - refunded: 0, - memory: MemoryGas::new(), - } - } - - /// Returns the gas limit. - #[inline] - pub const fn limit(&self) -> u64 { - self.limit - } - - /// Returns the **last** memory expansion cost. - #[inline] - #[deprecated = "memory expansion cost is not tracked anymore; \ - calculate it using `SharedMemory::current_expansion_cost` instead"] - #[doc(hidden)] - pub const fn memory(&self) -> u64 { - 0 - } - - /// Returns the total amount of gas that was refunded. - #[inline] - pub const fn refunded(&self) -> i64 { - self.refunded - } - - /// Returns the total amount of gas spent. - #[inline] - pub const fn spent(&self) -> u64 { - self.limit - self.remaining - } - - /// Returns the amount of gas remaining. - #[inline] - pub const fn remaining(&self) -> u64 { - self.remaining - } - - /// Return remaining gas after subtracting 63/64 parts. - pub const fn remaining_63_of_64_parts(&self) -> u64 { - self.remaining - self.remaining / 64 - } - - /// Erases a gas cost from the totals. - #[inline] - pub fn erase_cost(&mut self, returned: u64) { - self.remaining += returned; - } - - /// Spends all remaining gas. - #[inline] - pub fn spend_all(&mut self) { - self.remaining = 0; - } - - /// Records a refund value. - /// - /// `refund` can be negative but `self.refunded` should always be positive - /// at the end of transact. - #[inline] - pub fn record_refund(&mut self, refund: i64) { - self.refunded += refund; - } - - /// Set a refund value for final refund. - /// - /// Max refund value is limited to Nth part (depending of fork) of gas spend. - /// - /// Related to EIP-3529: Reduction in refunds - #[inline] - pub fn set_final_refund(&mut self, is_london: bool) { - let max_refund_quotient = if is_london { 5 } else { 2 }; - self.refunded = (self.refunded() as u64).min(self.spent() / max_refund_quotient) as i64; - } - - /// Set a refund value. This overrides the current refund value. - #[inline] - pub fn set_refund(&mut self, refund: i64) { - self.refunded = refund; - } - - /// Records an explicit cost. - /// - /// Returns `false` if the gas limit is exceeded. - #[inline] - #[must_use = "prefer using `gas!` instead to return an out-of-gas error on failure"] - pub fn record_cost(&mut self, cost: u64) -> bool { - let (remaining, overflow) = self.remaining.overflowing_sub(cost); - let success = !overflow; - if success { - self.remaining = remaining; - } - success - } - - /// Record memory expansion - #[inline] - #[must_use = "internally uses record_cost that flags out of gas error"] - pub fn record_memory_expansion(&mut self, new_len: usize) -> MemoryExtensionResult { - let Some(additional_cost) = self.memory.record_new_len(new_len) else { - return MemoryExtensionResult::Same; - }; - - if !self.record_cost(additional_cost) { - return MemoryExtensionResult::OutOfGas; - } - - MemoryExtensionResult::Extended - } -} - -pub enum MemoryExtensionResult { - /// Memory was extended. - Extended, - /// Memory size stayed the same. - Same, - /// Not enough gas to extend memory.s - OutOfGas, -} - -/// Utility struct that speeds up calculation of memory expansion -/// It contains the current memory length and its memory expansion cost. -/// -/// It allows us to split gas accounting from memory structure. -#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct MemoryGas { - /// Current memory length - pub words_num: usize, - /// Current memory expansion cost - pub expansion_cost: u64, -} - -impl MemoryGas { - pub const fn new() -> Self { - Self { - words_num: 0, - expansion_cost: 0, - } - } - - #[inline] - pub fn record_new_len(&mut self, new_num: usize) -> Option { - if new_num <= self.words_num { - return None; - } - self.words_num = new_num; - let mut cost = crate::gas::calc::memory_gas(new_num); - core::mem::swap(&mut self.expansion_cost, &mut cost); - // safe to subtract because we know that new_len > length - // notice the swap above. - Some(self.expansion_cost - cost) - } -} -``` - -> handler frame, in this case we want to look at `return_result()` fn: -```revm/crates/handler/src/frame.rs -use super::frame_data::*; -use bytecode::{Eof, EOF_MAGIC_BYTES}; -use context_interface::{ - journaled_state::{JournalCheckpoint, JournaledState}, - BlockGetter, Cfg, CfgGetter, ErrorGetter, JournalStateGetter, JournalStateGetterDBError, - Transaction, TransactionGetter, -}; -use core::{cell::RefCell, cmp::min}; -use handler_interface::{Frame, FrameOrResultGen, PrecompileProvider}; -use interpreter::{ - gas, - interpreter::{EthInterpreter, InstructionProvider}, - interpreter_types::{LoopControl, ReturnData, RuntimeFlag}, - return_ok, return_revert, CallInputs, CallOutcome, CallValue, CreateInputs, CreateOutcome, - CreateScheme, EOFCreateInputs, EOFCreateKind, FrameInput, Gas, Host, InputsImpl, - InstructionResult, Interpreter, InterpreterAction, InterpreterResult, InterpreterTypes, - SharedMemory, -}; -use precompile::PrecompileErrors; -use primitives::{keccak256, Address, Bytes, B256, U256}; -use specification::{ - constants::CALL_STACK_LIMIT, - hardfork::SpecId::{self, HOMESTEAD, LONDON, OSAKA, SPURIOUS_DRAGON}, -}; -use state::Bytecode; -use std::borrow::ToOwned; -use std::{rc::Rc, sync::Arc}; - -pub struct EthFrame { - _phantom: core::marker::PhantomData (CTX, ERROR)>, - data: FrameData, - // TODO include this - depth: usize, - /// Journal checkpoint. - pub checkpoint: JournalCheckpoint, - /// Interpreter. - pub interpreter: Interpreter, - /// Precompiles provider. - pub precompiles: PRECOMPILE, - /// Instruction provider. - pub instructions: INSTRUCTIONS, - // This is worth making as a generic type FrameSharedContext. - pub memory: Rc>, -} - -impl EthFrame -where - CTX: JournalStateGetter, - IW: InterpreterTypes, -{ - pub fn new( - data: FrameData, - depth: usize, - interpreter: Interpreter, - checkpoint: JournalCheckpoint, - precompiles: PRECOMP, - instructions: INST, - memory: Rc>, - ) -> Self { - Self { - _phantom: core::marker::PhantomData, - data, - depth, - interpreter, - checkpoint, - precompiles, - instructions, - memory, - } - } -} - -impl - EthFrame, PRECOMPILE, INSTRUCTION> -where - CTX: EthFrameContext, - ERROR: EthFrameError, - PRECOMPILE: PrecompileProvider, -{ - /// Make call frame - #[inline] - pub fn make_call_frame( - context: &mut CTX, - depth: usize, - memory: Rc>, - inputs: &CallInputs, - mut precompile: PRECOMPILE, - instructions: INSTRUCTION, - ) -> Result, ERROR> { - let gas = Gas::new(inputs.gas_limit); - - let return_result = |instruction_result: InstructionResult| { - Ok(FrameOrResultGen::Result(FrameResult::Call(CallOutcome { - result: InterpreterResult { - result: instruction_result, - gas, - output: Bytes::new(), - }, - memory_offset: inputs.return_memory_offset.clone(), - }))) - }; - - // Check depth - if depth > CALL_STACK_LIMIT as usize { - return return_result(InstructionResult::CallTooDeep); - } - - // Make account warm and loaded - let _ = context - .journal() - .load_account_delegated(inputs.bytecode_address)?; - - // Create subroutine checkpoint - let checkpoint = context.journal().checkpoint(); - - // Touch address. For "EIP-158 State Clear", this will erase empty accounts. - if let CallValue::Transfer(value) = inputs.value { - // Transfer value from caller to called account - // Target will get touched even if balance transferred is zero. - if let Some(i) = - context - .journal() - .transfer(&inputs.caller, &inputs.target_address, value)? - { - context.journal().checkpoint_revert(checkpoint); - return return_result(i.into()); - } - } - - if let Some(result) = precompile.run( - context, - &inputs.bytecode_address, - &inputs.input, - inputs.gas_limit, - )? { - if result.result.is_ok() { - context.journal().checkpoint_commit(); - } else { - context.journal().checkpoint_revert(checkpoint); - } - Ok(FrameOrResultGen::Result(FrameResult::Call(CallOutcome { - result, - memory_offset: inputs.return_memory_offset.clone(), - }))) - } else { - let account = context - .journal() - .load_account_code(inputs.bytecode_address)?; - - // TODO Request from foundry to get bytecode hash. - let _code_hash = account.info.code_hash(); - let mut bytecode = account.info.code.clone().unwrap_or_default(); - - // ExtDelegateCall is not allowed to call non-EOF contracts. - if inputs.scheme.is_ext_delegate_call() - && !bytecode.bytes_slice().starts_with(&EOF_MAGIC_BYTES) - { - return return_result(InstructionResult::InvalidExtDelegateCallTarget); - } - - if bytecode.is_empty() { - context.journal().checkpoint_commit(); - return return_result(InstructionResult::Stop); - } - - if let Bytecode::Eip7702(eip7702_bytecode) = bytecode { - bytecode = context - .journal() - .load_account_code(eip7702_bytecode.delegated_address)? - .info - .code - .clone() - .unwrap_or_default(); - } - - // Create interpreter and executes call and push new CallStackFrame. - let interpreter_input = InputsImpl { - target_address: inputs.target_address, - caller_address: inputs.caller, - input: inputs.input.clone(), - call_value: inputs.value.get(), - }; - - Ok(FrameOrResultGen::Frame(Self::new( - FrameData::Call(CallFrame { - return_memory_range: inputs.return_memory_offset.clone(), - }), - depth, - Interpreter::new( - memory.clone(), - bytecode, - interpreter_input, - inputs.is_static, - false, - context.cfg().spec().into(), - inputs.gas_limit, - ), - checkpoint, - precompile, - instructions, - memory, - ))) - } - } - - /// Make create frame. - #[inline] - pub fn make_create_frame( - context: &mut CTX, - depth: usize, - memory: Rc>, - inputs: &CreateInputs, - precompile: PRECOMPILE, - instructions: INSTRUCTION, - ) -> Result, ERROR> { - let spec = context.cfg().spec().into(); - let return_error = |e| { - Ok(FrameOrResultGen::Result(FrameResult::Create( - CreateOutcome { - result: InterpreterResult { - result: e, - gas: Gas::new(inputs.gas_limit), - output: Bytes::new(), - }, - address: None, - }, - ))) - }; - - // Check depth - if depth > CALL_STACK_LIMIT as usize { - return return_error(InstructionResult::CallTooDeep); - } - - // Prague EOF - if spec.is_enabled_in(OSAKA) && inputs.init_code.starts_with(&EOF_MAGIC_BYTES) { - return return_error(InstructionResult::CreateInitCodeStartingEF00); - } - - // Fetch balance of caller. - let caller_balance = context - .journal() - .load_account(inputs.caller)? - .map(|a| a.info.balance); - - // Check if caller has enough balance to send to the created contract. - if caller_balance.data < inputs.value { - return return_error(InstructionResult::OutOfFunds); - } - - // Increase nonce of caller and check if it overflows - let old_nonce; - if let Some(nonce) = context.journal().inc_account_nonce(inputs.caller)? { - old_nonce = nonce - 1; - } else { - return return_error(InstructionResult::Return); - } - - // Create address - // TODO incorporating code hash inside interpreter. It was a request by foundry. - let mut _init_code_hash = B256::ZERO; - let created_address = match inputs.scheme { - CreateScheme::Create => inputs.caller.create(old_nonce), - CreateScheme::Create2 { salt } => { - _init_code_hash = keccak256(&inputs.init_code); - inputs.caller.create2(salt.to_be_bytes(), _init_code_hash) - } - }; - - // created address is not allowed to be a precompile. - // TODO add precompile check - if precompile.contains(&created_address) { - return return_error(InstructionResult::CreateCollision); - } - - // warm load account. - context.journal().load_account(created_address)?; - - // create account, transfer funds and make the journal checkpoint. - let checkpoint = match context.journal().create_account_checkpoint( - inputs.caller, - created_address, - inputs.value, - spec, - ) { - Ok(checkpoint) => checkpoint, - Err(e) => return return_error(e.into()), - }; - - let bytecode = Bytecode::new_legacy(inputs.init_code.clone()); - - let interpreter_input = InputsImpl { - target_address: created_address, - caller_address: inputs.caller, - input: Bytes::new(), - call_value: inputs.value, - }; - - Ok(FrameOrResultGen::Frame(Self::new( - FrameData::Create(CreateFrame { created_address }), - depth, - Interpreter::new( - memory.clone(), - bytecode, - interpreter_input, - false, - false, - spec, - inputs.gas_limit, - ), - checkpoint, - precompile, - instructions, - memory, - ))) - } - - /// Make create frame. - #[inline] - pub fn make_eofcreate_frame( - context: &mut CTX, - depth: usize, - memory: Rc>, - inputs: &EOFCreateInputs, - precompile: PRECOMPILE, - instructions: INSTRUCTION, - ) -> Result, ERROR> { - let spec = context.cfg().spec().into(); - let return_error = |e| { - Ok(FrameOrResultGen::Result(FrameResult::EOFCreate( - CreateOutcome { - result: InterpreterResult { - result: e, - gas: Gas::new(inputs.gas_limit), - output: Bytes::new(), - }, - address: None, - }, - ))) - }; - - let (input, initcode, created_address) = match &inputs.kind { - EOFCreateKind::Opcode { - initcode, - input, - created_address, - } => (input.clone(), initcode.clone(), Some(*created_address)), - EOFCreateKind::Tx { initdata } => { - // decode eof and init code. - // TODO handle inc_nonce handling more gracefully. - let Ok((eof, input)) = Eof::decode_dangling(initdata.clone()) else { - context.journal().inc_account_nonce(inputs.caller)?; - return return_error(InstructionResult::InvalidEOFInitCode); - }; - - if eof.validate().is_err() { - // TODO (EOF) new error type. - context.journal().inc_account_nonce(inputs.caller)?; - return return_error(InstructionResult::InvalidEOFInitCode); - } - - // Use nonce from tx to calculate address. - let tx = context.tx().common_fields(); - let create_address = tx.caller().create(tx.nonce()); - - (input, eof, Some(create_address)) - } - }; - - // Check depth - if depth > CALL_STACK_LIMIT as usize { - return return_error(InstructionResult::CallTooDeep); - } - - // Fetch balance of caller. - let caller_balance = context - .journal() - .load_account(inputs.caller)? - .map(|a| a.info.balance); - - // Check if caller has enough balance to send to the created contract. - if caller_balance.data < inputs.value { - return return_error(InstructionResult::OutOfFunds); - } - - // Increase nonce of caller and check if it overflows - let Some(nonce) = context.journal().inc_account_nonce(inputs.caller)? else { - // can't happen on mainnet. - return return_error(InstructionResult::Return); - }; - let old_nonce = nonce - 1; - - let created_address = created_address.unwrap_or_else(|| inputs.caller.create(old_nonce)); - - // created address is not allowed to be a precompile. - if precompile.contains(&created_address) { - return return_error(InstructionResult::CreateCollision); - } - - // Load account so it needs to be marked as warm for access list. - context.journal().load_account(created_address)?; - - // create account, transfer funds and make the journal checkpoint. - let checkpoint = match context.journal().create_account_checkpoint( - inputs.caller, - created_address, - inputs.value, - spec, - ) { - Ok(checkpoint) => checkpoint, - Err(e) => return return_error(e.into()), - }; - - let interpreter_input = InputsImpl { - target_address: created_address, - caller_address: inputs.caller, - input, - call_value: inputs.value, - }; - - Ok(FrameOrResultGen::Frame(Self::new( - FrameData::EOFCreate(EOFCreateFrame { created_address }), - depth, - Interpreter::new( - memory.clone(), - Bytecode::Eof(Arc::new(initcode)), - interpreter_input, - false, - true, - spec, - inputs.gas_limit, - ), - checkpoint, - precompile, - instructions, - memory, - ))) - } - - pub fn init_with_context( - depth: usize, - frame_init: FrameInput, - memory: Rc>, - precompile: PRECOMPILE, - instructions: INSTRUCTION, - context: &mut CTX, - ) -> Result, ERROR> { - match frame_init { - FrameInput::Call(inputs) => { - Self::make_call_frame(context, depth, memory, &inputs, precompile, instructions) - } - FrameInput::Create(inputs) => { - Self::make_create_frame(context, depth, memory, &inputs, precompile, instructions) - } - FrameInput::EOFCreate(inputs) => Self::make_eofcreate_frame( - context, - depth, - memory, - &inputs, - precompile, - instructions, - ), - } - } -} - -impl Frame - for EthFrame, PRECOMPILE, INSTRUCTION> -where - CTX: EthFrameContext, - ERROR: EthFrameError, - PRECOMPILE: PrecompileProvider, - INSTRUCTION: InstructionProvider, Host = CTX>, -{ - type Context = CTX; - type Error = ERROR; - type FrameInit = FrameInput; - type FrameResult = FrameResult; - - fn init_first( - context: &mut Self::Context, - frame_input: Self::FrameInit, - ) -> Result, Self::Error> { - let memory = Rc::new(RefCell::new(SharedMemory::new())); - let precompiles = PRECOMPILE::new(context); - let instructions = INSTRUCTION::new(context); - - // load precompiles addresses as warm. - for address in precompiles.warm_addresses() { - context.journal().warm_account(address); - } - - memory.borrow_mut().new_context(); - Self::init_with_context(0, frame_input, memory, precompiles, instructions, context) - } - - fn init( - &self, - context: &mut CTX, - frame_init: Self::FrameInit, - ) -> Result, Self::Error> { - self.memory.borrow_mut().new_context(); - Self::init_with_context( - self.depth + 1, - frame_init, - self.memory.clone(), - self.precompiles.clone(), - self.instructions.clone(), - context, - ) - } - - fn run( - &mut self, - context: &mut Self::Context, - ) -> Result, Self::Error> { - let spec = context.cfg().spec().into(); - - // run interpreter - let next_action = self.interpreter.run(self.instructions.table(), context); - - let mut interpreter_result = match next_action { - InterpreterAction::NewFrame(new_frame) => { - return Ok(FrameOrResultGen::Frame(new_frame)) - } - InterpreterAction::Return { result } => result, - InterpreterAction::None => unreachable!("InterpreterAction::None is not expected"), - }; - - // Handle return from frame - let result = match &self.data { - FrameData::Call(frame) => { - // return_call - // revert changes or not. - if interpreter_result.result.is_ok() { - context.journal().checkpoint_commit(); - } else { - context.journal().checkpoint_revert(self.checkpoint); - } - FrameOrResultGen::Result(FrameResult::Call(CallOutcome::new( - interpreter_result, - frame.return_memory_range.clone(), - ))) - } - FrameData::Create(frame) => { - let max_code_size = context.cfg().max_code_size(); - return_create( - context.journal(), - self.checkpoint, - &mut interpreter_result, - frame.created_address, - max_code_size, - spec, - ); - - FrameOrResultGen::Result(FrameResult::Create(CreateOutcome::new( - interpreter_result, - Some(frame.created_address), - ))) - } - FrameData::EOFCreate(frame) => { - let max_code_size = context.cfg().max_code_size(); - return_eofcreate( - context.journal(), - self.checkpoint, - &mut interpreter_result, - frame.created_address, - max_code_size, - ); - - FrameOrResultGen::Result(FrameResult::EOFCreate(CreateOutcome::new( - interpreter_result, - Some(frame.created_address), - ))) - } - }; - - Ok(result) - } - - fn return_result( - &mut self, - context: &mut Self::Context, - result: Self::FrameResult, - ) -> Result<(), Self::Error> { - self.memory.borrow_mut().free_context(); - context.take_error()?; - - // Insert result to the top frame. - match result { - FrameResult::Call(outcome) => { - let out_gas = outcome.gas(); - let ins_result = *outcome.instruction_result(); - let returned_len = outcome.result.output.len(); - - let interpreter = &mut self.interpreter; - let mem_length = outcome.memory_length(); - let mem_start = outcome.memory_start(); - *interpreter.return_data.buffer_mut() = outcome.result.output; - - let target_len = min(mem_length, returned_len); - - if ins_result == InstructionResult::FatalExternalError { - panic!("Fatal external error in insert_call_outcome"); - } - - let item = { - if interpreter.runtime_flag.is_eof() { - match ins_result { - return_ok!() => U256::ZERO, - return_revert!() => U256::from(1), - _ => U256::from(2), - } - } else if ins_result.is_ok() { - U256::from(1) - } else { - U256::ZERO - } - }; - // Safe to push without stack limit check - let _ = interpreter.stack.push(item); - - // return unspend gas. - if ins_result.is_ok_or_revert() { - interpreter.control.gas().erase_cost(out_gas.remaining()); - self.memory - .borrow_mut() - .set(mem_start, &interpreter.return_data.buffer()[..target_len]); - } - - if ins_result.is_ok() { - interpreter.control.gas().record_refund(out_gas.refunded()); - } - } - FrameResult::Create(outcome) => { - let instruction_result = *outcome.instruction_result(); - let interpreter = &mut self.interpreter; - - let buffer = interpreter.return_data.buffer_mut(); - if instruction_result == InstructionResult::Revert { - // Save data to return data buffer if the create reverted - *buffer = outcome.output().to_owned() - } else { - // Otherwise clear it. Note that RETURN opcode should abort. - buffer.clear(); - }; - - assert_ne!( - instruction_result, - InstructionResult::FatalExternalError, - "Fatal external error in insert_eofcreate_outcome" - ); - - let this_gas = interpreter.control.gas(); - if instruction_result.is_ok_or_revert() { - this_gas.erase_cost(outcome.gas().remaining()); - } - - let stack_item = if instruction_result.is_ok() { - this_gas.record_refund(outcome.gas().refunded()); - outcome.address.unwrap_or_default().into_word().into() - } else { - U256::ZERO - }; - - // Safe to push without stack limit check - let _ = interpreter.stack.push(stack_item); - } - FrameResult::EOFCreate(outcome) => { - let instruction_result = *outcome.instruction_result(); - let interpreter = &mut self.interpreter; - if instruction_result == InstructionResult::Revert { - // Save data to return data buffer if the create reverted - *interpreter.return_data.buffer_mut() = outcome.output().to_owned() - } else { - // Otherwise clear it. Note that RETURN opcode should abort. - interpreter.return_data.buffer_mut().clear(); - }; - - assert_ne!( - instruction_result, - InstructionResult::FatalExternalError, - "Fatal external error in insert_eofcreate_outcome" - ); - - let this_gas = interpreter.control.gas(); - if instruction_result.is_ok_or_revert() { - this_gas.erase_cost(outcome.gas().remaining()); - } - - let stack_item = if instruction_result.is_ok() { - this_gas.record_refund(outcome.gas().refunded()); - outcome.address.expect("EOF Address").into_word().into() - } else { - U256::ZERO - }; - - // Safe to push without stack limit check - let _ = interpreter.stack.push(stack_item); - } - } - - Ok(()) - } -} - -pub fn return_create( - journal: &mut Journal, - checkpoint: JournalCheckpoint, - interpreter_result: &mut InterpreterResult, - address: Address, - max_code_size: usize, - spec_id: SpecId, -) { - // if return is not ok revert and return. - if !interpreter_result.result.is_ok() { - journal.checkpoint_revert(checkpoint); - return; - } - // Host error if present on execution - // if ok, check contract creation limit and calculate gas deduction on output len. - // - // EIP-3541: Reject new contract code starting with the 0xEF byte - if spec_id.is_enabled_in(LONDON) && interpreter_result.output.first() == Some(&0xEF) { - journal.checkpoint_revert(checkpoint); - interpreter_result.result = InstructionResult::CreateContractStartingWithEF; - return; - } - - // EIP-170: Contract code size limit - // By default limit is 0x6000 (~25kb) - if spec_id.is_enabled_in(SPURIOUS_DRAGON) && interpreter_result.output.len() > max_code_size { - journal.checkpoint_revert(checkpoint); - interpreter_result.result = InstructionResult::CreateContractSizeLimit; - return; - } - let gas_for_code = interpreter_result.output.len() as u64 * gas::CODEDEPOSIT; - if !interpreter_result.gas.record_cost(gas_for_code) { - // record code deposit gas cost and check if we are out of gas. - // EIP-2 point 3: If contract creation does not have enough gas to pay for the - // final gas fee for adding the contract code to the state, the contract - // creation fails (i.e. goes out-of-gas) rather than leaving an empty contract. - if spec_id.is_enabled_in(HOMESTEAD) { - journal.checkpoint_revert(checkpoint); - interpreter_result.result = InstructionResult::OutOfGas; - return; - } else { - interpreter_result.output = Bytes::new(); - } - } - // if we have enough gas we can commit changes. - journal.checkpoint_commit(); - - // Do analysis of bytecode straight away. - let bytecode = Bytecode::new_legacy(interpreter_result.output.clone()); - - // set code - journal.set_code(address, bytecode); - - interpreter_result.result = InstructionResult::Return; -} - -pub fn return_eofcreate( - journal: &mut Journal, - checkpoint: JournalCheckpoint, - interpreter_result: &mut InterpreterResult, - address: Address, - max_code_size: usize, -) { - // Note we still execute RETURN opcode and return the bytes. - // In EOF those opcodes should abort execution. - // - // In RETURN gas is still protecting us from ddos and in oog, - // behaviour will be same as if it failed on return. - // - // Bytes of RETURN will drained in `insert_eofcreate_outcome`. - if interpreter_result.result != InstructionResult::ReturnContract { - journal.checkpoint_revert(checkpoint); - return; - } - - if interpreter_result.output.len() > max_code_size { - journal.checkpoint_revert(checkpoint); - interpreter_result.result = InstructionResult::CreateContractSizeLimit; - return; - } - - // deduct gas for code deployment. - let gas_for_code = interpreter_result.output.len() as u64 * gas::CODEDEPOSIT; - if !interpreter_result.gas.record_cost(gas_for_code) { - journal.checkpoint_revert(checkpoint); - interpreter_result.result = InstructionResult::OutOfGas; - return; - } - - journal.checkpoint_commit(); - - // decode bytecode has a performance hit, but it has reasonable restrains. - let bytecode = Eof::decode(interpreter_result.output.clone()).expect("Eof is already verified"); - - // eof bytecode is going to be hashed. - journal.set_code(address, Bytecode::Eof(Arc::new(bytecode))); -} - -pub trait EthFrameContext: - TransactionGetter + Host + ErrorGetter + BlockGetter + JournalStateGetter + CfgGetter -{ -} - -impl< - ERROR, - CTX: TransactionGetter - + ErrorGetter - + BlockGetter - + JournalStateGetter - + CfgGetter - + Host, - > EthFrameContext for CTX -{ -} - -pub trait EthFrameError: - From> + From -{ -} - -impl> + From> - EthFrameError for T -{ -} +```mermaid +graph TD; + revm --> revm-r55 + rvemu-r55 --> revm-r55 + eth-riscv-runtime --> revm-r55 ``` diff --git a/contract-derive/src/lib.rs b/contract-derive/src/lib.rs index d312038..8bfd5be 100644 --- a/contract-derive/src/lib.rs +++ b/contract-derive/src/lib.rs @@ -278,10 +278,6 @@ fn is_payable(method: &syn::ImplItemMethod) -> bool { #[proc_macro_attribute] pub fn interface(_attr: TokenStream, item: TokenStream) -> TokenStream { - // DEBUG - println!("\n=== Input Trait ==="); - println!("{}", item.to_string()); - let input = parse_macro_input!(item as ItemTrait); let trait_name = &input.ident; @@ -296,12 +292,6 @@ pub fn interface(_attr: TokenStream, item: TokenStream) -> TokenStream { .unwrap_or_default(); let method_selector = u32::from_be_bytes(selector_bytes); - // DEBUG - println!("\n=== Processing Method ==="); - println!("Method name: {}", method_name); - println!("Selector bytes: {:02x?}", selector_bytes); - println!("Selector u32: {}", method_selector); - // Extract argument types and names, skipping self let arg_types: Vec<_> = method .sig @@ -405,9 +395,5 @@ pub fn interface(_attr: TokenStream, item: TokenStream) -> TokenStream { } }; - // DEBUG - println!("\n=== Generated Code ==="); - println!("{:#}", expanded.to_string().replace(';', ";\n")); - TokenStream::from(expanded) } diff --git a/r55/src/exec.rs b/r55/src/exec.rs index 722f748..2caab5e 100644 --- a/r55/src/exec.rs +++ b/r55/src/exec.rs @@ -5,15 +5,15 @@ use eth_riscv_syscalls::Syscall; use revm::{ handler::register::EvmHandler, interpreter::{ - CallInputs, CallScheme, CallValue, Gas, Host, InstructionResult, Interpreter, - InterpreterAction, InterpreterResult, SharedMemory, + CallInputs, CallScheme, CallValue, Host, InstructionResult, Interpreter, InterpreterAction, + InterpreterResult, SharedMemory, }, primitives::{address, Address, Bytes, ExecutionResult, Log, Output, TransactTo, B256, U256}, Database, Evm, Frame, FrameOrResult, InMemoryDB, }; use rvemu::{emulator::Emulator, exception::Exception}; use std::{collections::BTreeMap, rc::Rc, sync::Arc}; -use tracing::{debug, field::debug, warn}; +use tracing::{debug, warn}; use super::error::{Error, Result, TxResult}; use super::gas; From 110cc5addd481368723eb0b992f081231c2b8d7d Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Sat, 14 Dec 2024 01:28:23 +0100 Subject: [PATCH 05/11] chore: split into erc20 + erc20x --- Cargo.toml | 2 +- erc20/src/lib.rs | 19 ------------------- erc20x/.cargo/config | 8 ++++++++ erc20x/Cargo.toml | 23 +++++++++++++++++++++++ erc20x/src/deploy.rs | 25 +++++++++++++++++++++++++ erc20x/src/lib.rs | 31 +++++++++++++++++++++++++++++++ r55/src/exec.rs | 13 +++++-------- r55/tests/e2e.rs | 21 ++++++++++++++++----- 8 files changed, 109 insertions(+), 33 deletions(-) create mode 100644 erc20x/.cargo/config create mode 100644 erc20x/Cargo.toml create mode 100644 erc20x/src/deploy.rs create mode 100644 erc20x/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 8bd7d8c..6a02765 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ resolver = "2" members = ["eth-riscv-interpreter", "eth-riscv-syscalls", "r55"] default-members = ["eth-riscv-interpreter", "eth-riscv-syscalls", "r55"] -exclude = ["contract-derive", "erc20", "eth-riscv-runtime"] +exclude = ["contract-derive", "erc20", "erc20x", "eth-riscv-runtime"] [workspace.package] version = "0.1.0" diff --git a/erc20/src/lib.rs b/erc20/src/lib.rs index 7ce56ed..857aabd 100644 --- a/erc20/src/lib.rs +++ b/erc20/src/lib.rs @@ -20,17 +20,6 @@ pub struct ERC20 { symbol: String, decimals: u8, } -#[derive(Event)] -pub struct DebugCalldata { - #[indexed] - pub target: Address, - pub calldata: Vec, -} - -#[interface] -trait IERC20 { - fn balance_of(&self, owner: Address) -> u64; -} #[derive(Event)] pub struct Transfer { @@ -52,14 +41,6 @@ pub struct Mint { #[contract] impl ERC20 { - pub fn x_balance_of(&self, owner: Address, target: Address) -> u64 { - let token = IERC20::new(target); - match token.balance_of(owner) { - Some(balance) => balance, - _ => eth_riscv_runtime::revert(), - } - } - pub fn balance_of(&self, owner: Address) -> u64 { self.balances.read(owner) } diff --git a/erc20x/.cargo/config b/erc20x/.cargo/config new file mode 100644 index 0000000..8222a7a --- /dev/null +++ b/erc20x/.cargo/config @@ -0,0 +1,8 @@ +[target.riscv64imac-unknown-none-elf] +rustflags = [ + "-C", "link-arg=-T../r5-rust-rt.x", + "-C", "inline-threshold=275" +] + +[build] +target = "riscv64imac-unknown-none-elf" diff --git a/erc20x/Cargo.toml b/erc20x/Cargo.toml new file mode 100644 index 0000000..0b24bdf --- /dev/null +++ b/erc20x/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "erc20x" +version = "0.1.0" +edition = "2021" + +[dependencies] +contract-derive = { path = "../contract-derive" } +eth-riscv-runtime = { path = "../eth-riscv-runtime" } + +alloy-core = { version = "0.7.4", default-features = false } +alloy-sol-types = { version = "0.7.4", default-features = false } + +[[bin]] +name = "runtime" +path = "src/lib.rs" + +[[bin]] +name = "deploy" +path = "src/deploy.rs" + +[profile.release] +lto = true +opt-level = "z" diff --git a/erc20x/src/deploy.rs b/erc20x/src/deploy.rs new file mode 100644 index 0000000..0e04ea2 --- /dev/null +++ b/erc20x/src/deploy.rs @@ -0,0 +1,25 @@ +#![no_std] +#![no_main] + +extern crate alloc; +use alloc::vec::Vec; + +use eth_riscv_runtime::return_riscv; + +#[eth_riscv_runtime::entry] +fn main() -> ! +{ + //decode constructor arguments + //constructor(ars); + let runtime: &[u8] = include_bytes!("../target/riscv64imac-unknown-none-elf/release/runtime"); + + let mut prepended_runtime = Vec::with_capacity(1 + runtime.len()); + prepended_runtime.push(0xff); + prepended_runtime.extend_from_slice(runtime); + + let prepended_runtime_slice: &[u8] = &prepended_runtime; + + let result_ptr = prepended_runtime_slice.as_ptr() as u64; + let result_len = prepended_runtime_slice.len() as u64; + return_riscv(result_ptr, result_len); +} diff --git a/erc20x/src/lib.rs b/erc20x/src/lib.rs new file mode 100644 index 0000000..1810a4a --- /dev/null +++ b/erc20x/src/lib.rs @@ -0,0 +1,31 @@ +#![no_std] +#![no_main] + +use core::default::Default; + +use contract_derive::{contract, interface, payable, Event}; +use eth_riscv_runtime::types::Mapping; + +use alloy_core::primitives::{address, Address, U256}; + +extern crate alloc; +use alloc::{string::String, vec::Vec}; + +#[derive(Default)] +pub struct ERC20x; + +#[interface] +trait IERC20 { + fn balance_of(&self, owner: Address) -> u64; +} + +#[contract] +impl ERC20x { + pub fn x_balance_of(&self, owner: Address, target: Address) -> u64 { + let token = IERC20::new(target); + match token.balance_of(owner) { + Some(balance) => balance, + _ => eth_riscv_runtime::revert(), + } + } +} diff --git a/r55/src/exec.rs b/r55/src/exec.rs index 2caab5e..f74fcb0 100644 --- a/r55/src/exec.rs +++ b/r55/src/exec.rs @@ -117,8 +117,7 @@ pub fn handle_register(handler: &mut EvmHandler<'_, EXT, DB>) handler.execution.call = Arc::new(move |ctx, inputs| { let result = old_handle(ctx, inputs); if let Ok(FrameOrResult::Frame(frame)) = &result { - let context = riscv_context(frame); - call_stack_inner.borrow_mut().push(context); + call_stack_inner.borrow_mut().push(riscv_context(frame)); } result }); @@ -138,6 +137,7 @@ pub fn handle_register(handler: &mut EvmHandler<'_, EXT, DB>) let old_handle = handler.execution.execute_frame.clone(); handler.execution.execute_frame = Arc::new(move |frame, memory, instraction_table, ctx| { let depth = call_stack.borrow().len() - 1; + // use last frame as stack is FIFO let mut result = if let Some(Some(riscv_context)) = call_stack.borrow_mut().last_mut() { debug!( @@ -182,7 +182,7 @@ pub fn handle_register(handler: &mut EvmHandler<'_, EXT, DB>) if res.output.len() == return_memory.len() { return_memory.copy_from_slice(&res.output); } else { - warn!("Unexpected output size!") + warn!("Unexpected return data size!"); } } } @@ -198,7 +198,7 @@ pub fn handle_register(handler: &mut EvmHandler<'_, EXT, DB>) fn execute_riscv( rvemu: &mut RVEmu, interpreter: &mut Interpreter, - shared_memory: &mut SharedMemory, + _shared_memory: &mut SharedMemory, host: &mut dyn Host, ) -> Result { debug!( @@ -218,9 +218,6 @@ fn execute_riscv( let returned_data_destiny = &mut rvemu.returned_data_destiny; if let Some(destiny) = std::mem::take(returned_data_destiny) { let data = emu.cpu.bus.get_dram_slice(destiny)?; - if shared_memory.len() >= data.len() { - data.copy_from_slice(shared_memory.slice(0, data.len())); - } debug!("Loaded return data: {}", Bytes::copy_from_slice(data)); } @@ -255,7 +252,7 @@ fn execute_riscv( let ret_size: u64 = emu.cpu.xregs.read(11); let r55_gas = r55_gas_used(&emu.cpu.inst_counter); - debug!("> total R55 gas: {}", r55_gas); + debug!("> Total R55 gas: {}", r55_gas); // RETURN logs the gas of the whole risc-v instruction set syscall_gas!(interpreter, r55_gas); diff --git a/r55/tests/e2e.rs b/r55/tests/e2e.rs index 30ad3fb..8ab477a 100644 --- a/r55/tests/e2e.rs +++ b/r55/tests/e2e.rs @@ -12,6 +12,7 @@ use revm::{ use tracing::{debug, error, info}; const ERC20_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../erc20"); +const ERC20X_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../erc20x"); #[test] fn erc20() { @@ -20,8 +21,9 @@ fn erc20() { let mut db = InMemoryDB::default(); let bytecode = compile_with_prefix(compile_deploy, ERC20_PATH).unwrap(); - let addr1 = deploy_contract(&mut db, bytecode.clone()).unwrap(); - let addr2 = deploy_contract(&mut db, bytecode).unwrap(); + let bytecode_x = compile_with_prefix(compile_deploy, ERC20X_PATH).unwrap(); + let addr1 = deploy_contract(&mut db, bytecode).unwrap(); + let addr2 = deploy_contract(&mut db, bytecode_x).unwrap(); let selector_balance = get_selector_from_sig("balance_of"); let selector_x_balance = get_selector_from_sig("x_balance_of"); @@ -52,7 +54,10 @@ fn erc20() { ); match run_tx(&mut db, &addr1, complete_calldata_mint.clone()) { Ok(res) => info!("Success! {}", res), - Err(e) => error!("Error when executing tx! {:#?}", e), + Err(e) => { + error!("Error when executing tx! {:#?}", e); + panic!() + } }; info!("----------------------------------------------------------"); @@ -64,7 +69,10 @@ fn erc20() { ); match run_tx(&mut db, &addr1, complete_calldata_balance.clone()) { Ok(res) => info!("Success! {}", res), - Err(e) => error!("Error when executing tx! {:#?}", e), + Err(e) => { + error!("Error when executing tx! {:#?}", e); + panic!() + } }; info!("----------------------------------------------------------"); @@ -76,6 +84,9 @@ fn erc20() { ); match run_tx(&mut db, &addr2, complete_calldata_x_balance.clone()) { Ok(res) => info!("Success! {}", res), - Err(e) => error!("Error when executing tx! {:#?}", e), + Err(e) => { + error!("Error when executing tx! {:#?}", e); + panic!(); + } } } From 7e8eebd29ce061aea541fe33707b0d00c7c0900f Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Sat, 14 Dec 2024 02:29:10 +0100 Subject: [PATCH 06/11] chore: housekeeping --- .zed/settings.json | 0 erc20/src/lib.rs | 4 ++-- eth-riscv-runtime/src/{types/mapping.rs => types.rs} | 0 eth-riscv-runtime/src/types/mod.rs | 3 --- r55/src/exec.rs | 2 +- 5 files changed, 3 insertions(+), 6 deletions(-) create mode 100644 .zed/settings.json rename eth-riscv-runtime/src/{types/mapping.rs => types.rs} (100%) delete mode 100644 eth-riscv-runtime/src/types/mod.rs diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 0000000..e69de29 diff --git a/erc20/src/lib.rs b/erc20/src/lib.rs index 857aabd..75ba1c4 100644 --- a/erc20/src/lib.rs +++ b/erc20/src/lib.rs @@ -3,13 +3,13 @@ use core::default::Default; -use contract_derive::{contract, interface, payable, Event}; +use contract_derive::{contract, payable, Event}; use eth_riscv_runtime::types::Mapping; use alloy_core::primitives::{address, Address, U256}; extern crate alloc; -use alloc::{string::String, vec::Vec}; +use alloc::string::String; #[derive(Default)] pub struct ERC20 { diff --git a/eth-riscv-runtime/src/types/mapping.rs b/eth-riscv-runtime/src/types.rs similarity index 100% rename from eth-riscv-runtime/src/types/mapping.rs rename to eth-riscv-runtime/src/types.rs diff --git a/eth-riscv-runtime/src/types/mod.rs b/eth-riscv-runtime/src/types/mod.rs deleted file mode 100644 index 3034c0e..0000000 --- a/eth-riscv-runtime/src/types/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod mapping; - -pub use mapping::{Mapping, StorageStorable}; diff --git a/r55/src/exec.rs b/r55/src/exec.rs index f74fcb0..1b3a1f0 100644 --- a/r55/src/exec.rs +++ b/r55/src/exec.rs @@ -138,7 +138,7 @@ pub fn handle_register(handler: &mut EvmHandler<'_, EXT, DB>) handler.execution.execute_frame = Arc::new(move |frame, memory, instraction_table, ctx| { let depth = call_stack.borrow().len() - 1; - // use last frame as stack is FIFO + // use last frame as stack is LIFO let mut result = if let Some(Some(riscv_context)) = call_stack.borrow_mut().last_mut() { debug!( "=== [FRAME-{}] Contract: {} ============-", From 17137329db40f8463763d1e8f6c8fd75853893d7 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Sat, 14 Dec 2024 18:56:46 +0100 Subject: [PATCH 07/11] chore: housekeeping --- .zed/settings.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .zed/settings.json diff --git a/.zed/settings.json b/.zed/settings.json deleted file mode 100644 index e69de29..0000000 From 93c8ddd1d85405bd768687d38c47dff9c41801bc Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Sun, 15 Dec 2024 19:00:25 +0100 Subject: [PATCH 08/11] chore: enhance contract macro --- Cargo.toml | 8 +- contract-derive/src/helpers.rs | 130 +++++++++++++++++++++ contract-derive/src/lib.rs | 198 ++++++++++---------------------- erc20/Cargo.toml | 4 + erc20x/Cargo.toml | 1 + erc20x/src/lib.rs | 11 +- erc20x_standalone/.cargo/config | 8 ++ erc20x_standalone/Cargo.toml | 23 ++++ erc20x_standalone/src/deploy.rs | 25 ++++ erc20x_standalone/src/lib.rs | 29 +++++ r55/tests/e2e.rs | 7 +- r55/tests/interface.rs | 34 ++++++ 12 files changed, 331 insertions(+), 147 deletions(-) create mode 100644 contract-derive/src/helpers.rs create mode 100644 erc20x_standalone/.cargo/config create mode 100644 erc20x_standalone/Cargo.toml create mode 100644 erc20x_standalone/src/deploy.rs create mode 100644 erc20x_standalone/src/lib.rs create mode 100644 r55/tests/interface.rs diff --git a/Cargo.toml b/Cargo.toml index 6a02765..83ce59e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,13 @@ resolver = "2" members = ["eth-riscv-interpreter", "eth-riscv-syscalls", "r55"] default-members = ["eth-riscv-interpreter", "eth-riscv-syscalls", "r55"] -exclude = ["contract-derive", "erc20", "erc20x", "eth-riscv-runtime"] +exclude = [ + "contract-derive", + "erc20", + "erc20x", + "erc20x_standalone", + "eth-riscv-runtime", +] [workspace.package] version = "0.1.0" diff --git a/contract-derive/src/helpers.rs b/contract-derive/src/helpers.rs new file mode 100644 index 0000000..610f98a --- /dev/null +++ b/contract-derive/src/helpers.rs @@ -0,0 +1,130 @@ +use alloy_core::primitives::keccak256; +use alloy_sol_types::SolValue; +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{FnArg, Ident, ImplItemMethod, ReturnType, TraitItemMethod}; + +// Method info that we need from `ImplItemMethod` and `TraitItemMethod` +pub struct MethodInfo<'a> { + name: &'a Ident, + args: Vec, + return_type: &'a ReturnType, +} + +impl<'a> From<&'a ImplItemMethod> for MethodInfo<'a> { + fn from(method: &'a ImplItemMethod) -> Self { + Self { + name: &method.sig.ident, + args: method.sig.inputs.iter().skip(1).cloned().collect(), + return_type: &method.sig.output, + } + } +} + +impl<'a> From<&'a TraitItemMethod> for MethodInfo<'a> { + fn from(method: &'a TraitItemMethod) -> Self { + Self { + name: &method.sig.ident, + args: method.sig.inputs.iter().skip(1).cloned().collect(), + return_type: &method.sig.output, + } + } +} + +// Helper function to generate intercate impl from user-defined methods +pub fn generate_interface( + methods: &[&T], + interface_name: &Ident, +) -> quote::__private::TokenStream +where + for<'a> MethodInfo<'a>: From<&'a T>, +{ + let methods: Vec = methods.iter().map(|&m| MethodInfo::from(m)).collect(); + + // Generate implementation + let method_impls = methods.iter().map(|method| { + let name = method.name; + let args = &method.args; + let return_type = method.return_type; + let method_selector = u32::from_be_bytes( + keccak256(name.to_string())[..4] + .try_into() + .unwrap_or_default(), + ); + + // Simply use index for arg names, and extract types + let (arg_names, arg_types): (Vec<_>, Vec<_>) = args + .iter() + .enumerate() + .map(|(i, arg)| { + if let FnArg::Typed(pat_type) = arg { + let ty = &*pat_type.ty; + (format_ident!("arg{}", i), ty) + } else { + panic!("Expected typed arguments"); + } + }) + .unzip(); + + let calldata = if arg_names.is_empty() { + quote! { + let mut complete_calldata = Vec::with_capacity(4); + complete_calldata.extend_from_slice(&[ + #method_selector.to_be_bytes()[0], + #method_selector.to_be_bytes()[1], + #method_selector.to_be_bytes()[2], + #method_selector.to_be_bytes()[3], + ]); + } + } else { + quote! { + let mut args_calldata = (#(#arg_names),*).abi_encode(); + let mut complete_calldata = Vec::with_capacity(4 + args_calldata.len()); + complete_calldata.extend_from_slice(&[ + #method_selector.to_be_bytes()[0], + #method_selector.to_be_bytes()[1], + #method_selector.to_be_bytes()[2], + #method_selector.to_be_bytes()[3], + ]); + complete_calldata.append(&mut args_calldata); + } + }; + + let return_ty = match return_type { + ReturnType::Default => quote! { () }, + ReturnType::Type(_, ty) => quote! { #ty }, + }; + + quote! { + pub fn #name(&self, #(#arg_names: #arg_types),*) -> Option<#return_ty> { + use alloy_sol_types::SolValue; + use alloc::vec::Vec; + + #calldata + + let result = eth_riscv_runtime::call_contract( + self.address, + 0_u64, + &complete_calldata, + 32_u64 + )?; + + <#return_ty>::abi_decode(&result, true).ok() + } + } + }); + + quote! { + pub struct #interface_name { + address: Address, + } + + impl #interface_name { + pub fn new(address: Address) -> Self { + Self { address } + } + + #(#method_impls)* + } + } +} diff --git a/contract-derive/src/lib.rs b/contract-derive/src/lib.rs index 8bfd5be..d4217b6 100644 --- a/contract-derive/src/lib.rs +++ b/contract-derive/src/lib.rs @@ -1,10 +1,13 @@ extern crate proc_macro; use alloy_core::primitives::keccak256; -use alloy_sol_types::SolValue; use proc_macro::TokenStream; use quote::{format_ident, quote}; -use syn::{parse_macro_input, Data, DeriveInput, Fields, ImplItem, ItemImpl, ItemTrait, TraitItem}; -use syn::{FnArg, ReturnType}; +use syn::{ + parse_macro_input, Data, DeriveInput, Fields, FnArg, ImplItem, ImplItemMethod, ItemImpl, + ItemTrait, ReturnType, TraitItem, +}; + +mod helpers; #[proc_macro_derive(Event, attributes(indexed))] pub fn event_derive(input: TokenStream) -> TokenStream { @@ -100,13 +103,13 @@ pub fn contract(_attr: TokenStream, item: TokenStream) -> TokenStream { panic!("Expected a struct."); }; - let mut public_methods = Vec::new(); + let mut public_methods: Vec<&ImplItemMethod> = Vec::new(); // Iterate over the items in the impl block to find pub methods for item in input.items.iter() { if let ImplItem::Method(method) = item { if let syn::Visibility::Public(_) = method.vis { - public_methods.push(method.clone()); + public_methods.push(method); } } } @@ -217,44 +220,64 @@ pub fn contract(_attr: TokenStream, item: TokenStream) -> TokenStream { } }; - // Generate the call method implementation - let call_method = quote! { - use alloy_sol_types::SolValue; - use eth_riscv_runtime::*; + // Generate the interface + let interface_name = format_ident!("I{}", struct_name); + let interface = helpers::generate_interface(&public_methods, &interface_name); - #emit_helper - impl Contract for #struct_name { - fn call(&self) { - self.call_with_data(&msg_data()); - } + // Generate the complete output with module structure + let output = quote! { + // Public interface module + pub mod interface { + use super::*; + #interface + } + + // Generate the call method implementation privately + only when necessarys + #[cfg(not(feature = "interface-only"))] + mod implementation { + use super::*; + use alloy_sol_types::SolValue; + use eth_riscv_runtime::*; + + #input + + #emit_helper + + impl Contract for #struct_name { + fn call(&self) { + self.call_with_data(&msg_data()); + } + + fn call_with_data(&self, calldata: &[u8]) { + let selector = u32::from_be_bytes([calldata[0], calldata[1], calldata[2], calldata[3]]); + let calldata = &calldata[4..]; - fn call_with_data(&self, calldata: &[u8]) { - let selector = u32::from_be_bytes([calldata[0], calldata[1], calldata[2], calldata[3]]); - let calldata = &calldata[4..]; + match selector { + #( #match_arms )* + _ => revert(), + } - match selector { - #( #match_arms )* - _ => revert(), + return_riscv(0, 0); } + } - return_riscv(0, 0); + #[eth_riscv_runtime::entry] + fn main() -> ! { + let contract = #struct_name::default(); + contract.call(); + eth_riscv_runtime::return_riscv(0, 0) } } - #[eth_riscv_runtime::entry] - fn main() -> ! - { - let contract = #struct_name::default(); - contract.call(); - eth_riscv_runtime::return_riscv(0, 0) - } - }; + // Always export the interface + pub use interface::*; - let output = quote! { - #input - #call_method + // Only export contract impl when not in `interface-only` mode + #[cfg(not(feature = "interface-only"))] + pub use implementation::*; }; + // Convert the output to TokenStream TokenStream::from(output) } @@ -281,119 +304,24 @@ pub fn interface(_attr: TokenStream, item: TokenStream) -> TokenStream { let input = parse_macro_input!(item as ItemTrait); let trait_name = &input.ident; - let method_impls: Vec<_> = input + let methods: Vec<_> = input .items .iter() .map(|item| { if let TraitItem::Method(method) = item { - let method_name = &method.sig.ident; - let selector_bytes = keccak256(method_name.to_string())[..4] - .try_into() - .unwrap_or_default(); - let method_selector = u32::from_be_bytes(selector_bytes); - - // Extract argument types and names, skipping self - let arg_types: Vec<_> = method - .sig - .inputs - .iter() - .skip(1) - .map(|arg| { - if let FnArg::Typed(pat_type) = arg { - let ty = &*pat_type.ty; - quote! { #ty } - } else { - panic!("Expected typed arguments"); - } - }) - .collect(); - let arg_names: Vec<_> = (0..method.sig.inputs.len() - 1) - .map(|i| format_ident!("arg{}", i)) - .collect(); - - // Get the return type - let return_type = match &method.sig.output { - ReturnType::Default => quote! { () }, - ReturnType::Type(_, ty) => - quote! { #ty }, - }; - - // Generate calldata with different encoding depending on # of args - let args_encoding = if arg_names.is_empty() { - quote! { - let mut complete_calldata = Vec::with_capacity(4); - complete_calldata.extend_from_slice(&[ - #method_selector.to_be_bytes()[0], - #method_selector.to_be_bytes()[1], - #method_selector.to_be_bytes()[2], - #method_selector.to_be_bytes()[3], - ]); - } - } else if arg_names.len() == 1 { - quote! { - let mut args_calldata = #(#arg_names),*.abi_encode(); - let mut complete_calldata = Vec::with_capacity(4 + args_calldata.len()); - complete_calldata.extend_from_slice(&[ - #method_selector.to_be_bytes()[0], - #method_selector.to_be_bytes()[1], - #method_selector.to_be_bytes()[2], - #method_selector.to_be_bytes()[3], - ]); - complete_calldata.append(&mut args_calldata); - } - } else { - quote! { - let mut args_calldata = (#(#arg_names),*).abi_encode(); - let mut complete_calldata = Vec::with_capacity(4 + args_calldata.len()); - complete_calldata.extend_from_slice(&[ - #method_selector.to_be_bytes()[0], - #method_selector.to_be_bytes()[1], - #method_selector.to_be_bytes()[2], - #method_selector.to_be_bytes()[3], - ]); - complete_calldata.append(&mut args_calldata); - } - }; - - Some(quote! { - pub fn #method_name(&self, #(#arg_names: #arg_types),*) -> Option<#return_type> { - use alloy_sol_types::SolValue; - use alloc::vec::Vec; - - #args_encoding - - // Make the call - let result = eth_riscv_runtime::call_contract( - self.address, - 0_u64, - &complete_calldata, - 32_u64 // TODO: Figure out how to use SolType to get the return size - - )?; - - // Decode result - <#return_type>::abi_decode(&result, true).ok() - } - }) + method } else { - panic!("Expected methods arguments"); + panic!("Expected methods arguments") } }) .collect(); - let expanded = quote! { - pub struct #trait_name { - address: Address, - } - - impl #trait_name { - pub fn new(address: Address) -> Self { - Self { address } - } + // Generate intreface implementation + let interface = helpers::generate_interface(&methods, trait_name); - #(#method_impls)* - } + let output = quote! { + #interface }; - TokenStream::from(expanded) + TokenStream::from(output) } diff --git a/erc20/Cargo.toml b/erc20/Cargo.toml index a5c4303..e30b7db 100644 --- a/erc20/Cargo.toml +++ b/erc20/Cargo.toml @@ -3,6 +3,10 @@ name = "erc20" version = "0.1.0" edition = "2021" +[features] +default = [] +interface-only = [] + [dependencies] contract-derive = { path = "../contract-derive" } eth-riscv-runtime = { path = "../eth-riscv-runtime" } diff --git a/erc20x/Cargo.toml b/erc20x/Cargo.toml index 0b24bdf..f7be4c3 100644 --- a/erc20x/Cargo.toml +++ b/erc20x/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] contract-derive = { path = "../contract-derive" } eth-riscv-runtime = { path = "../eth-riscv-runtime" } +erc20 = { path = "../erc20", features = ["interface-only"] } alloy-core = { version = "0.7.4", default-features = false } alloy-sol-types = { version = "0.7.4", default-features = false } diff --git a/erc20x/src/lib.rs b/erc20x/src/lib.rs index 1810a4a..1019a7e 100644 --- a/erc20x/src/lib.rs +++ b/erc20x/src/lib.rs @@ -3,22 +3,17 @@ use core::default::Default; -use contract_derive::{contract, interface, payable, Event}; -use eth_riscv_runtime::types::Mapping; - use alloy_core::primitives::{address, Address, U256}; +use contract_derive::{contract, interface}; extern crate alloc; use alloc::{string::String, vec::Vec}; +use erc20::IERC20; + #[derive(Default)] pub struct ERC20x; -#[interface] -trait IERC20 { - fn balance_of(&self, owner: Address) -> u64; -} - #[contract] impl ERC20x { pub fn x_balance_of(&self, owner: Address, target: Address) -> u64 { diff --git a/erc20x_standalone/.cargo/config b/erc20x_standalone/.cargo/config new file mode 100644 index 0000000..8222a7a --- /dev/null +++ b/erc20x_standalone/.cargo/config @@ -0,0 +1,8 @@ +[target.riscv64imac-unknown-none-elf] +rustflags = [ + "-C", "link-arg=-T../r5-rust-rt.x", + "-C", "inline-threshold=275" +] + +[build] +target = "riscv64imac-unknown-none-elf" diff --git a/erc20x_standalone/Cargo.toml b/erc20x_standalone/Cargo.toml new file mode 100644 index 0000000..00fc961 --- /dev/null +++ b/erc20x_standalone/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "erc20x_standalone" +version = "0.1.0" +edition = "2021" + +[dependencies] +contract-derive = { path = "../contract-derive" } +eth-riscv-runtime = { path = "../eth-riscv-runtime" } + +alloy-core = { version = "0.7.4", default-features = false } +alloy-sol-types = { version = "0.7.4", default-features = false } + +[[bin]] +name = "runtime" +path = "src/lib.rs" + +[[bin]] +name = "deploy" +path = "src/deploy.rs" + +[profile.release] +lto = true +opt-level = "z" diff --git a/erc20x_standalone/src/deploy.rs b/erc20x_standalone/src/deploy.rs new file mode 100644 index 0000000..0e04ea2 --- /dev/null +++ b/erc20x_standalone/src/deploy.rs @@ -0,0 +1,25 @@ +#![no_std] +#![no_main] + +extern crate alloc; +use alloc::vec::Vec; + +use eth_riscv_runtime::return_riscv; + +#[eth_riscv_runtime::entry] +fn main() -> ! +{ + //decode constructor arguments + //constructor(ars); + let runtime: &[u8] = include_bytes!("../target/riscv64imac-unknown-none-elf/release/runtime"); + + let mut prepended_runtime = Vec::with_capacity(1 + runtime.len()); + prepended_runtime.push(0xff); + prepended_runtime.extend_from_slice(runtime); + + let prepended_runtime_slice: &[u8] = &prepended_runtime; + + let result_ptr = prepended_runtime_slice.as_ptr() as u64; + let result_len = prepended_runtime_slice.len() as u64; + return_riscv(result_ptr, result_len); +} diff --git a/erc20x_standalone/src/lib.rs b/erc20x_standalone/src/lib.rs new file mode 100644 index 0000000..7d59af9 --- /dev/null +++ b/erc20x_standalone/src/lib.rs @@ -0,0 +1,29 @@ +#![no_std] +#![no_main] + +use core::default::Default; + +use alloy_core::primitives::{address, Address, U256}; +use contract_derive::{contract, interface}; + +extern crate alloc; +use alloc::{string::String, vec::Vec}; + +#[derive(Default)] +pub struct ERC20x; + +#[interface] +trait IERC20 { + fn balance_of(&self, owner: Address) -> u64; +} + +#[contract] +impl ERC20x { + pub fn x_balance_of(&self, owner: Address, target: Address) -> u64 { + let token = IERC20::new(target); + match token.balance_of(owner) { + Some(balance) => balance, + _ => eth_riscv_runtime::revert(), + } + } +} diff --git a/r55/tests/e2e.rs b/r55/tests/e2e.rs index 8ab477a..46548a6 100644 --- a/r55/tests/e2e.rs +++ b/r55/tests/e2e.rs @@ -13,6 +13,7 @@ use tracing::{debug, error, info}; const ERC20_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../erc20"); const ERC20X_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../erc20x"); +const ERC20X_ALONE_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../erc20x_standalone"); #[test] fn erc20() { @@ -53,7 +54,7 @@ fn erc20() { Bytes::from(complete_calldata_mint.clone()) ); match run_tx(&mut db, &addr1, complete_calldata_mint.clone()) { - Ok(res) => info!("Success! {}", res), + Ok(res) => info!("{}", res), Err(e) => { error!("Error when executing tx! {:#?}", e); panic!() @@ -68,7 +69,7 @@ fn erc20() { Bytes::from(complete_calldata_balance.clone()) ); match run_tx(&mut db, &addr1, complete_calldata_balance.clone()) { - Ok(res) => info!("Success! {}", res), + Ok(res) => info!("{}", res), Err(e) => { error!("Error when executing tx! {:#?}", e); panic!() @@ -83,7 +84,7 @@ fn erc20() { Bytes::from(complete_calldata_x_balance.clone()) ); match run_tx(&mut db, &addr2, complete_calldata_x_balance.clone()) { - Ok(res) => info!("Success! {}", res), + Ok(res) => info!("{}", res), Err(e) => { error!("Error when executing tx! {:#?}", e); panic!(); diff --git a/r55/tests/interface.rs b/r55/tests/interface.rs new file mode 100644 index 0000000..a82c407 --- /dev/null +++ b/r55/tests/interface.rs @@ -0,0 +1,34 @@ +use r55::{ + compile_deploy, compile_with_prefix, exec::deploy_contract, test_utils::initialize_logger, +}; +use revm::InMemoryDB; +use tracing::{error, info}; + +const ERC20X_ALONE_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../erc20x_standalone"); + +#[test] +fn deploy_erc20x_without_contract_dependencies() { + initialize_logger(); + + let mut db = InMemoryDB::default(); + + info!( + "Compiling erc20x using user-defined IERC20 (without contract dependencies). Contract path: {}", + ERC20X_ALONE_PATH + ); + let bytecode = match compile_with_prefix(compile_deploy, ERC20X_ALONE_PATH) { + Ok(code) => code, + Err(e) => { + error!("Failed to compile ERC20X: {:?}", e); + panic!("ERC20X compilation failed"); + } + }; + + match deploy_contract(&mut db, bytecode) { + Ok(addr) => info!("Contract deployed at {}", addr), + Err(e) => { + error!("Failed to deploy ERC20X: {:?}", e); + panic!("ERC20X deployment failed") + } + } +} From f8ec238f9d1981dc126116bb5084ec8a3b6c2665 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Sun, 15 Dec 2024 19:06:34 +0100 Subject: [PATCH 09/11] style: comments --- contract-derive/src/helpers.rs | 2 +- contract-derive/src/lib.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contract-derive/src/helpers.rs b/contract-derive/src/helpers.rs index 610f98a..8d7b0d4 100644 --- a/contract-derive/src/helpers.rs +++ b/contract-derive/src/helpers.rs @@ -4,7 +4,7 @@ use proc_macro::TokenStream; use quote::{format_ident, quote}; use syn::{FnArg, Ident, ImplItemMethod, ReturnType, TraitItemMethod}; -// Method info that we need from `ImplItemMethod` and `TraitItemMethod` +// Unified method info from `ImplItemMethod` and `TraitItemMethod` pub struct MethodInfo<'a> { name: &'a Ident, args: Vec, diff --git a/contract-derive/src/lib.rs b/contract-derive/src/lib.rs index d4217b6..12a3840 100644 --- a/contract-derive/src/lib.rs +++ b/contract-derive/src/lib.rs @@ -232,7 +232,8 @@ pub fn contract(_attr: TokenStream, item: TokenStream) -> TokenStream { #interface } - // Generate the call method implementation privately + only when necessarys + // Generate the call method implementation privately + // only when not in `interface-only` mode #[cfg(not(feature = "interface-only"))] mod implementation { use super::*; @@ -277,7 +278,6 @@ pub fn contract(_attr: TokenStream, item: TokenStream) -> TokenStream { pub use implementation::*; }; - // Convert the output to TokenStream TokenStream::from(output) } From a0de7f2e29c271e074e751eeeb5b81e79490a3cc Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Mon, 16 Dec 2024 19:12:01 +0100 Subject: [PATCH 10/11] style: incorporate feedback --- .gitignore | 3 ++ r55/rvemu.dtb | Bin 1598 -> 0 bytes r55/rvemu.dts | 88 ------------------------------------------ r55/src/exec.rs | 8 ++-- r55/src/test_utils.rs | 1 - rvemu.dts | 88 ------------------------------------------ 6 files changed, 8 insertions(+), 180 deletions(-) delete mode 100644 r55/rvemu.dtb delete mode 100644 r55/rvemu.dts delete mode 100644 rvemu.dts diff --git a/.gitignore b/.gitignore index 6985cf1..87f0419 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ Cargo.lock # MSVC Windows builds of rustc generate these, which store debugging information *.pdb + +# Ignore RISCV emulator files +**/rvemu.* diff --git a/r55/rvemu.dtb b/r55/rvemu.dtb deleted file mode 100644 index 47c9f54671a68d8e9b2f850ba5abb28a3ae680bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1598 zcmah}J#Q2-5cL8H1c(TNz5=;-OFQMBiEl2vxs8{2#0s8Uc+ra}~S z)U^BsT7Cc>HT(p`d-nQn6Ob5b&NK7o%j4Pmw*LF45UbCG5c@)`JjMAKd>_041|`>E zzY*k1ze%TE#~E?>im2a%9QAd`15;QzOJ{{~g@#U|?*s6oJ~lj4RZA4b<%zbc_A-4R zf`>+Hcd(tS+4d~YHjUnty0*Gh2hPo3juyVGKi+OL<0dH2FH86 z=;Wxs68jUFJkA+aRJvh7@?)Xz8hB9KoxSmRltCu>+F=5RRaI!;8M_Pcq9%J_qFA%v zL$7A6>QDOK1rI|0A@f5GHD%LUDxjE?a=!-WMT+>D`0)N5dV<)r${J#cEL-c88BqZx zQXADNG43txVBR${b4;%Iz3cp9+y#7KTGaP*?erC#AN6TArvr#FtKhIxv;-UX?30=8 z?G{ybluO&Kk@vp??6cpqp`Gnw+d)ZD<2I&kT#6-fMwg{B9$EJBg??bxBz*-_=Db4v zAt!`P5|(8g?-%bM&!ke7HuK4TZy}lMCio&?kV1|PoPGx9w194fgSir`eKE%V@7v?O z2ezo1Ql-;w3m8ZG4*us1CbIX%T)q=yW8EK+$DO|DmrF?M8lx|xlXJ(&&k=95C)td|PJrjlf3N z$ZK0!o*QLtDpgrpv1N-ls*|Ozn#>i`Qi)3EsuWZFXJzKt3hQ#+x)kV%SS+7iia?C2 zosgxLr|FSVXRWH`To`pMTv4fMW>wG3Z>BlP>Kv8uk61``=;n<=nats>!zc@ziK=Q- gk8CZfcD?rW; - #size-cells = <0x02>; - compatible = "riscv-virtio"; - model = "riscv-virtio,qemu"; - - chosen { - bootargs = "root=/dev/vda ro console=ttyS0"; - stdout-path = "/uart@10000000"; - }; - - uart@10000000 { - interrupts = <0xa>; - interrupt-parent = <0x03>; - clock-frequency = <0x384000>; - reg = <0x0 0x10000000 0x0 0x100>; - compatible = "ns16550a"; - }; - - virtio_mmio@10001000 { - interrupts = <0x01>; - interrupt-parent = <0x03>; - reg = <0x0 0x10001000 0x0 0x1000>; - compatible = "virtio,mmio"; - }; - - cpus { - #address-cells = <0x01>; - #size-cells = <0x00>; - timebase-frequency = <0x989680>; - - cpu-map { - cluster0 { - core0 { - cpu = <0x01>; - }; - }; - }; - - cpu@0 { - phandle = <0x01>; - device_type = "cpu"; - reg = <0x00>; - status = "okay"; - compatible = "riscv"; - riscv,isa = "rv64imafdcsu"; - mmu-type = "riscv,sv48"; - - interrupt-controller { - #interrupt-cells = <0x01>; - interrupt-controller; - compatible = "riscv,cpu-intc"; - phandle = <0x02>; - }; - }; - }; - - memory@80000000 { - device_type = "memory"; - reg = <0x0 0x80000000 0x0 0x8000000>; - }; - - soc { - #address-cells = <0x02>; - #size-cells = <0x02>; - compatible = "simple-bus"; - ranges; - - interrupt-controller@c000000 { - phandle = <0x03>; - riscv,ndev = <0x35>; - reg = <0x00 0xc000000 0x00 0x4000000>; - interrupts-extended = <0x02 0x0b 0x02 0x09>; - interrupt-controller; - compatible = "riscv,plic0"; - #interrupt-cells = <0x01>; - #address-cells = <0x00>; - }; - - clint@2000000 { - interrupts-extended = <0x02 0x03 0x02 0x07>; - reg = <0x00 0x2000000 0x00 0x10000>; - compatible = "riscv,clint0"; - }; - }; -}; \ No newline at end of file diff --git a/r55/src/exec.rs b/r55/src/exec.rs index 1b3a1f0..b43b8dc 100644 --- a/r55/src/exec.rs +++ b/r55/src/exec.rs @@ -13,12 +13,14 @@ use revm::{ }; use rvemu::{emulator::Emulator, exception::Exception}; use std::{collections::BTreeMap, rc::Rc, sync::Arc}; -use tracing::{debug, warn}; +use tracing::{debug, trace, warn}; use super::error::{Error, Result, TxResult}; use super::gas; use super::syscall_gas; +const R5_REST_OF_RAM_INIT: u64 = 0x80300000; // Defined at `r5-rust-rt.x` + pub fn deploy_contract(db: &mut InMemoryDB, bytecode: Bytes) -> Result
{ let mut evm = Evm::builder() .with_db(db) @@ -203,7 +205,7 @@ fn execute_riscv( ) -> Result { debug!( "{} RISC-V execution: PC: {:#x} Return data dst: {:#?}", - if rvemu.emu.cpu.pc == 0x80300000 { + if rvemu.emu.cpu.pc == R5_REST_OF_RAM_INIT { "Starting" } else { "Resuming" @@ -547,7 +549,7 @@ fn execute_riscv( } } Ok(_) => { - debug!("Successful instruction at PC: {:#x}", emu.cpu.pc); + trace!("Successful instruction at PC: {:#x}", emu.cpu.pc); continue; } Err(e) => { diff --git a/r55/src/test_utils.rs b/r55/src/test_utils.rs index 4f84161..68c3196 100644 --- a/r55/src/test_utils.rs +++ b/r55/src/test_utils.rs @@ -16,7 +16,6 @@ pub fn initialize_logger() { log_level ))) .with_target(false) - // .without_time() .finish(); tracing::subscriber::set_global_default(tracing_sub) .expect("Setting tracing subscriber failed"); diff --git a/rvemu.dts b/rvemu.dts deleted file mode 100644 index c33186f..0000000 --- a/rvemu.dts +++ /dev/null @@ -1,88 +0,0 @@ -/dts-v1/; - -/ { - #address-cells = <0x02>; - #size-cells = <0x02>; - compatible = "riscv-virtio"; - model = "riscv-virtio,qemu"; - - chosen { - bootargs = "root=/dev/vda ro console=ttyS0"; - stdout-path = "/uart@10000000"; - }; - - uart@10000000 { - interrupts = <0xa>; - interrupt-parent = <0x03>; - clock-frequency = <0x384000>; - reg = <0x0 0x10000000 0x0 0x100>; - compatible = "ns16550a"; - }; - - virtio_mmio@10001000 { - interrupts = <0x01>; - interrupt-parent = <0x03>; - reg = <0x0 0x10001000 0x0 0x1000>; - compatible = "virtio,mmio"; - }; - - cpus { - #address-cells = <0x01>; - #size-cells = <0x00>; - timebase-frequency = <0x989680>; - - cpu-map { - cluster0 { - core0 { - cpu = <0x01>; - }; - }; - }; - - cpu@0 { - phandle = <0x01>; - device_type = "cpu"; - reg = <0x00>; - status = "okay"; - compatible = "riscv"; - riscv,isa = "rv64imafdcsu"; - mmu-type = "riscv,sv48"; - - interrupt-controller { - #interrupt-cells = <0x01>; - interrupt-controller; - compatible = "riscv,cpu-intc"; - phandle = <0x02>; - }; - }; - }; - - memory@80000000 { - device_type = "memory"; - reg = <0x0 0x80000000 0x0 0x8000000>; - }; - - soc { - #address-cells = <0x02>; - #size-cells = <0x02>; - compatible = "simple-bus"; - ranges; - - interrupt-controller@c000000 { - phandle = <0x03>; - riscv,ndev = <0x35>; - reg = <0x00 0xc000000 0x00 0x4000000>; - interrupts-extended = <0x02 0x0b 0x02 0x09>; - interrupt-controller; - compatible = "riscv,plic0"; - #interrupt-cells = <0x01>; - #address-cells = <0x00>; - }; - - clint@2000000 { - interrupts-extended = <0x02 0x03 0x02 0x07>; - reg = <0x00 0x2000000 0x00 0x10000>; - compatible = "riscv,clint0"; - }; - }; -}; \ No newline at end of file From 3b386585200ad689703a647bf2af693c8ddcca9e Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Mon, 16 Dec 2024 23:33:09 +0100 Subject: [PATCH 11/11] style: clippy --- r55/src/test_utils.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/r55/src/test_utils.rs b/r55/src/test_utils.rs index 68c3196..c69233e 100644 --- a/r55/src/test_utils.rs +++ b/r55/src/test_utils.rs @@ -11,10 +11,7 @@ pub fn initialize_logger() { let log_level = std::env::var("RUST_LOG").unwrap_or("INFO".to_owned()); let tracing_sub = tracing_subscriber::fmt() .with_max_level(tracing::Level::DEBUG) - .with_env_filter(tracing_subscriber::EnvFilter::new(&format!( - "{}", - log_level - ))) + .with_env_filter(tracing_subscriber::EnvFilter::new(log_level)) .with_target(false) .finish(); tracing::subscriber::set_global_default(tracing_sub)