Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Syscall Log & Event Automation #14

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
157 changes: 157 additions & 0 deletions contract-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,168 @@ 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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's for sure an alloy function that does this conversion

// Address
s if s.contains("Address") || 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") || s.contains("uint256") => 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") || s.contains("int256") => 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<u8>") => b"bytes",
s if s.contains("String") || s.contains("str") => b"string",

// Dynamic arrays
s if s.contains("Vec<Address>") => b"address[]",
s if s.contains("Vec<U256>") => b"uint256[]",
s if s.contains("Vec<bool>") => b"bool[]",
s if s.contains("Vec<B256>") => 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 {
// Handle multiple arguments: emit!(event_name, idx arg1, arg2, idx arg3, ...)
($event_name:expr, $($val:tt)+) => {{
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"(");

// Initialize topics[0] for signature hash
let mut first = true;
let mut topics = alloc::vec![B256::default()];
let mut data = Vec::new();

process_args!(signature, first, topics, data, $($val)+);

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);
}
}};

// Handle single argument: emit!(event_name, arg)
($event_name:expr, $val: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"(");
signature.extend_from_slice(get_type_signature!($val));
signature.extend_from_slice(b")");

let topic0 = B256::from(keccak256(&signature));
let topics = alloc::vec![topic0];

let encoded = $val.abi_encode();
eth_riscv_runtime::emit_log(&encoded, &topics);
}};
}

#[macro_export]
macro_rules! process_args {
// Process final non-indexed value
($sig:expr, $first:expr, $topics:expr, $data:expr, $val:expr) => {{
if !$first { $sig.extend_from_slice(b","); }
$sig.extend_from_slice(get_type_signature!($val));
let encoded = $val.abi_encode();
$data.extend_from_slice(&encoded);
}};

// Process final indexed value (idx)
($sig:expr, $first:expr, $topics:expr, $data:expr, idx $val:expr) => {{
if !$first { $sig.extend_from_slice(b","); }
$sig.extend_from_slice(get_type_signature!($val));
let encoded = $val.abi_encode();
if $topics.len() < 4 { // EVM limit: max 4 topics
$topics.push(B256::from_slice(&encoded));
}
}};

// Process indexed value recursively
($sig:expr, $first:expr, $topics:expr, $data:expr, idx $val:expr, $($rest:tt)+) => {{
if !$first { $sig.extend_from_slice(b","); }
$first = false;
$sig.extend_from_slice(get_type_signature!($val));
let encoded = $val.abi_encode();
if $topics.len() < 4 {
$topics.push(B256::from_slice(&encoded));
}
process_args!($sig, $first, $topics, $data, $($rest)+);
}};

// Process non-indexed value recursively
($sig:expr, $first:expr, $topics:expr, $data:expr, $val:expr, $($rest:tt)+) => {{
if !$first { $sig.extend_from_slice(b","); }
$first = false;
$sig.extend_from_slice(get_type_signature!($val));
let encoded = $val.abi_encode();
$data.extend_from_slice(&encoded);
process_args!($sig, $first, $topics, $data, $($rest)+);
}};
}
};

// 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());
Expand Down
31 changes: 29 additions & 2 deletions erc20/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use contract_derive::{contract, payable};
use eth_riscv_runtime::types::Mapping;

use alloy_core::primitives::{Address, address, U256};
extern crate alloc;

#[derive(Default)]
pub struct ERC20 {
Expand All @@ -19,7 +20,7 @@ impl ERC20 {
self.balance.read(owner)
}

pub fn transfer(&self, from: Address, to: Address, value: u64) {
pub fn transfer(&self, from: Address, to: Address, value: u64) -> bool {
let from_balance = self.balance.read(from);
let to_balance = self.balance.read(to);

Expand All @@ -29,16 +30,42 @@ impl ERC20 {

self.balance.write(from, from_balance - value);
self.balance.write(to, to_balance + value);

emit!("Transfer", idx from, idx to, value);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it the case that in Solidity one defines which fields are indexed when declaring the event, not when using it? What will happen if another emit! call doesn't do idx from? Or change the (type) signature of the Transfer event entirely.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I completely understand what you're saying. We could do something more similar to what we have with Solidity and write it that way.

It's interesting to hear your opinions to define what would be most appropriate in this case. All of this is provisional. We could do something like this. What do you think?

#[event]
struct Transfer {
    #[indexed]
    from: Address,   
    #[indexed]
    to: Address,      
    value: U256    
}

pub fn transfer(&self, from: Address, to: Address, value: u64) -> bool {
    ...
    emit!(Transfer, from, to, value);
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, struct makes more sense.

As for emitting there a couple of options:

  1. What you did above.
  2. Construct a Transfer instance and pass that to the emit!
  3. Have event generate a transfer! macro (named after the struct).

Although one might argue that 2&3 are just sugar in top of 1.

Copy link
Author

@scab24 scab24 Nov 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just uploaded the update, would the structure be something like this?

Now I need to test everything byte by byte to ensure that everything matches 100% of the time

c48a335

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's awesome @scab24 , thanks for doing this.

Please, don't treat everything I write as a command :) I really appreciate jumping on this immediately but I don't want to add you work unnecessarily. Let's see what @gakonst and @leonardoalt think as well.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@deuszx
Thank you for your comments, I'm glad to do it, it's a really interesting project 😀

Whenever I have some free time at work, I spend a bit on it

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually like the struct idea. What I don't necessarily like 100% is the emit! macro, because the type isn't being used by the user. Currently it's being used like this:

emit!(Transfer, from, to, value);

In this version, the type is only coupled with the arguments because of the macro. In this case we don't really need the struct to exist, the macro could do everything.

log::emit(Transfer::new(from, to, value));

An alternative to that would be the above, where the user gives the full instance, and uses a function instead of a macro. The function could rely on a trait "EncodeLog" (for example), which is implemented for the type when deriving Event.

@scab24 let's all agree on a solution here before you implement anything

@gakonst wdyt?

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();
}

let to_balance = self.balance.read(to);
self.balance.write(to, to_balance + value);
emit!("Mint", idx 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", idx 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
}
}
16 changes: 16 additions & 0 deletions eth-riscv-runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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() {
Expand Down
17 changes: 17 additions & 0 deletions eth-riscv-runtime/src/log.rs
Original file line number Diff line number Diff line change
@@ -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());
}
Comment on lines +5 to +9
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is topics an unbounded slice if only the first 3 elements are used?


crate::log(
data.as_ptr() as u64,
data.len() as u64,
all_topics.as_ptr() as u64,
topics.len() as u64
);
}
1 change: 1 addition & 0 deletions eth-riscv-syscalls/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,5 @@ syscalls!(
(0xf1, Call, "call"),
(0xf3, Return, "return"),
(0xfd, Revert, "revert"),
(0xA0, Log, "log"),
);
52 changes: 48 additions & 4 deletions r55/src/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<u8>,
pub logs: Vec<Log>
}

pub fn run_tx(db: &mut InMemoryDB, addr: &Address, calldata: Vec<u8>) {
pub fn run_tx(db: &mut InMemoryDB, addr: &Address, calldata: Vec<u8>) -> TxResult {
let mut evm = Evm::builder()
.with_db(db)
.modify_tx_env(|tx| {
Expand All @@ -57,10 +61,17 @@ pub fn run_tx(db: &mut InMemoryDB, addr: &Address, calldata: Vec<u8>) {
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)]
Expand Down Expand Up @@ -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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's a helper for this now

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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This if is not needed, the for loop already covers the 0 case

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(),
));
}
}
}
_ => {
Expand Down
Loading