diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..d66b54c Binary files /dev/null and b/.DS_Store differ diff --git a/contract-derive/src/lib.rs b/contract-derive/src/lib.rs index 1028295..83f4c37 100644 --- a/contract-derive/src/lib.rs +++ b/contract-derive/src/lib.rs @@ -2,9 +2,44 @@ extern crate proc_macro; use alloy_core::primitives::keccak256; use proc_macro::TokenStream; use quote::{format_ident, quote}; -use syn::{parse_macro_input, ImplItem, ItemImpl}; +use syn::{parse_macro_input, ImplItem, ItemImpl, DeriveInput, Data, Fields}; 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"); + }; + + 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)),* + ]; + } + }; + + TokenStream::from(expanded) +} + #[proc_macro_attribute] pub fn show_streams(attr: TokenStream, item: TokenStream) -> TokenStream { println!("attr: \"{}\"", attr.to_string()); @@ -87,11 +122,118 @@ pub fn contract(_attr: TokenStream, item: TokenStream) -> TokenStream { } }).collect(); + let emit_helper = quote! { + #[macro_export] + macro_rules! get_type_signature { + ($arg:expr) => { + match stringify!($arg) { + // Address + s if s.contains("Address") => b"address", + + // Unsigned integers + s if s.contains("u8") => b"uint8", + s if s.contains("u16") => b"uint16", + s if s.contains("u32") => b"uint32", + s if s.contains("u64") => b"uint64", + s if s.contains("u128") => b"uint128", + s if s.contains("U256") => b"uint256", + + // Signed integers + s if s.contains("i8") => b"int8", + s if s.contains("i16") => b"int16", + s if s.contains("i32") => b"int32", + s if s.contains("i64") => b"int64", + s if s.contains("i128") => b"int128", + s if s.contains("I256") => b"int256", + + // Boolean + s if s.contains("bool") => b"bool", + + // Bytes y FixedBytes + s if s.contains("B256") => b"bytes32", + s if s.contains("[u8; 32]") => b"bytes32", + s if s.contains("[u8; 20]") => b"bytes20", + s if s.contains("[u8; 16]") => b"bytes16", + s if s.contains("[u8; 8]") => b"bytes8", + s if s.contains("[u8; 4]") => b"bytes4", + s if s.contains("[u8; 1]") => b"bytes1", + + // Dynamic bytes & strings + s if s.contains("Vec") => b"bytes", + s if s.contains("String") => b"string", + + // Dynamic arrays + s if s.contains("Vec
") => b"address[]", + s if s.contains("Vec") => b"uint256[]", + s if s.contains("Vec") => b"bool[]", + s if s.contains("Vec") => b"bytes32[]", + + // Static arrays + s if s.contains("[Address; ") => b"address[]", + s if s.contains("[U256; ") => b"uint256[]", + s if s.contains("[bool; ") => b"bool[]", + s if s.contains("[B256; ") => b"bytes32[]", + + // Tuples + s if s.contains("(Address, U256)") => b"(address,uint256)", + s if s.contains("(U256, bool)") => b"(uint256,bool)", + s if s.contains("(Address, Address)") => b"(address,address)", + + _ => b"uint64", + } + }; + } + + #[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(); + + // AquĆ­ debes mapear $field a el identificador correspondiente en $event + 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()); @@ -143,4 +285,4 @@ fn is_payable(method: &syn::ImplItemMethod) -> bool { } false }) -} +} \ No newline at end of file diff --git a/erc20/src/lib.rs b/erc20/src/lib.rs index 0848cca..8518c68 100644 --- a/erc20/src/lib.rs +++ b/erc20/src/lib.rs @@ -3,14 +3,50 @@ use core::default::Default; -use contract_derive::{contract, payable}; +use contract_derive::{contract, payable, Event}; use eth_riscv_runtime::types::Mapping; use alloy_core::primitives::{Address, address, U256}; +extern crate alloc; #[derive(Default)] pub struct ERC20 { balance: Mapping, + paused: bool, + metadata: [u8; 32], +} + +#[derive(Event)] +pub struct Transfer { + #[indexed] + pub from: Address, + #[indexed] + pub to: Address, + pub value: u64 +} + +#[derive(Event)] +pub struct Mint { + #[indexed] + pub to: Address, + pub value: u64 +} + +#[derive(Event)] +pub struct Burn { + #[indexed] + pub from: Address, + pub value: u64 +} + +#[derive(Event)] +pub struct PauseChanged { + pub paused: bool +} + +#[derive(Event)] +pub struct MetadataUpdated { + pub data: [u8; 32] } #[contract] @@ -19,7 +55,8 @@ impl ERC20 { self.balance.read(owner) } - pub fn transfer(&self, from: Address, to: Address, value: u64) { + pub fn transfer(&self, to: Address, value: u64) -> bool { + let from = msg_sender(); let from_balance = self.balance.read(from); let to_balance = self.balance.read(to); @@ -29,10 +66,13 @@ impl ERC20 { self.balance.write(from, from_balance - value); self.balance.write(to, to_balance + value); + + emit!(Transfer, from, to, value); + true } #[payable] - pub fn mint(&self, to: Address, value: u64) { + pub fn mint(&self, to: Address, value: u64) -> bool { let owner = msg_sender(); if owner != address!("0000000000000000000000000000000000000007") { revert(); @@ -40,5 +80,32 @@ impl ERC20 { let to_balance = self.balance.read(to); self.balance.write(to, to_balance + value); + + emit!(Mint, to, value); + true } -} + + pub fn burn(&self, from: Address, value: u64) -> bool { + let from_balance = self.balance.read(from); + if from_balance < value { + revert(); + } + + self.balance.write(from, from_balance - value); + + emit!(Burn, from, value); + true + } + + pub fn set_paused(&self, paused: bool) -> bool { + + emit!(PauseChanged, paused); + true + } + + pub fn update_metadata(&self, data: [u8; 32]) -> bool { + + emit!(MetadataUpdated, data); + true + } +} \ No newline at end of file diff --git a/eth-riscv-runtime/src/lib.rs b/eth-riscv-runtime/src/lib.rs index f97dcbe..6e9ce88 100644 --- a/eth-riscv-runtime/src/lib.rs +++ b/eth-riscv-runtime/src/lib.rs @@ -12,6 +12,9 @@ mod alloc; pub mod types; pub mod block; +mod log; +pub use log::emit_log; + const CALLDATA_ADDRESS: usize = 0x8000_0000; pub trait Contract { @@ -141,6 +144,19 @@ pub fn msg_data() -> &'static [u8] { 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() { diff --git a/eth-riscv-runtime/src/log.rs b/eth-riscv-runtime/src/log.rs new file mode 100644 index 0000000..1e8aed1 --- /dev/null +++ b/eth-riscv-runtime/src/log.rs @@ -0,0 +1,17 @@ +use alloy_core::primitives::B256; + +pub fn emit_log(data: &[u8], topics: &[B256]) { + let mut all_topics = [0u8; 96]; + for (i, topic) in topics.iter().enumerate() { + if i >= 3 { break; } + let start = i * 32; + all_topics[start..start + 32].copy_from_slice(topic.as_ref()); + } + + crate::log( + data.as_ptr() as u64, + data.len() as u64, + all_topics.as_ptr() as u64, + topics.len() as u64 + ); +} \ No newline at end of file diff --git a/eth-riscv-syscalls/src/lib.rs b/eth-riscv-syscalls/src/lib.rs index 10dd5e1..a1dcb63 100644 --- a/eth-riscv-syscalls/src/lib.rs +++ b/eth-riscv-syscalls/src/lib.rs @@ -71,4 +71,5 @@ syscalls!( (0xf1, Call, "call"), (0xf3, Return, "return"), (0xfd, Revert, "revert"), + (0xA0, Log, "log"), ); diff --git a/r55/src/exec.rs b/r55/src/exec.rs index 203bdb9..1a75017 100644 --- a/r55/src/exec.rs +++ b/r55/src/exec.rs @@ -7,7 +7,7 @@ use revm::{ CallInputs, CallScheme, CallValue, Host, InstructionResult, Interpreter, InterpreterAction, InterpreterResult, SharedMemory, }, - primitives::{address, Address, Bytes, ExecutionResult, Output, TransactTo, U256}, + primitives::{address, Address, Bytes, ExecutionResult, Output, TransactTo, U256, B256, Log}, Database, Evm, Frame, FrameOrResult, InMemoryDB, }; use rvemu::{emulator::Emulator, exception::Exception}; @@ -39,8 +39,12 @@ pub fn deploy_contract(db: &mut InMemoryDB, bytecode: Bytes) -> Address { result => panic!("Unexpected result: {:?}", result), } } +pub struct TxResult { + pub output: Vec, + pub logs: Vec +} -pub fn run_tx(db: &mut InMemoryDB, addr: &Address, calldata: Vec) { +pub fn run_tx(db: &mut InMemoryDB, addr: &Address, calldata: Vec) -> TxResult { let mut evm = Evm::builder() .with_db(db) .modify_tx_env(|tx| { @@ -57,10 +61,17 @@ pub fn run_tx(db: &mut InMemoryDB, addr: &Address, calldata: Vec) { match result { ExecutionResult::Success { output: Output::Call(value), + logs, .. - } => println!("Tx result: {:?}", value), + } => { + println!("Tx result: {:?}", value); + TxResult { + output:value.into(), + logs + } + }, result => panic!("Unexpected result: {:?}", result), - }; + } } #[derive(Debug)] @@ -324,6 +335,39 @@ fn execute_riscv( emu.cpu.xregs.write(12, limbs[2]); emu.cpu.xregs.write(13, limbs[3]); } + 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); + + let data = if data_size > 0 { + emu.cpu.bus + .get_dram_slice(data_ptr..(data_ptr + data_size)) + .unwrap_or_default() + .to_vec() + } else { + vec![] + }; + + let mut topics = Vec::new(); + if topics_size > 0 { + for i in 0..topics_size { + let topic_ptr = topics_ptr + (i * 32); + if let Ok(topic_data) = emu.cpu.bus + .get_dram_slice(topic_ptr..(topic_ptr + 32)) + { + topics.push(B256::from_slice(topic_data)); + } + } + } + + host.log(Log::new_unchecked( + interpreter.contract.target_address, + topics, + data.into(), + )); + } } } _ => { diff --git a/r55/src/main.rs b/r55/src/main.rs index febfb3d..384aaa1 100644 --- a/r55/src/main.rs +++ b/r55/src/main.rs @@ -10,6 +10,7 @@ use revm::{ primitives::{address, keccak256, ruint::Uint, AccountInfo, Address, Bytecode, Bytes}, InMemoryDB, }; +use alloy_core::hex; fn compile_runtime(path: &str) -> Result, ()> { println!("Compiling runtime: {}", path); @@ -183,6 +184,25 @@ fn test_deploy() { test_runtime(&addr, &mut db); } + +////////////////////////// +/// TESTS // +////////////////////////// + +fn parse_hex_result(result: &[u8]) -> u64 { + let mut bytes = [0u8; 8]; + bytes.copy_from_slice(&result[24..32]); + u64::from_be_bytes(bytes) +} + +fn parse_bool_result(hex_result: &[u8]) -> bool { + if hex_result.len() == 32 { + hex_result[31] == 1 + } else { + false + } +} + fn main() { test_runtime_from_binary(); test_deploy(); diff --git a/rvemu.dtb b/rvemu.dtb new file mode 100644 index 0000000..47c9f54 Binary files /dev/null and b/rvemu.dtb differ