-
Notifications
You must be signed in to change notification settings - Fork 15
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
feat: impl contract calls #26
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
concept ack
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 | ||
let return_memory = parent | ||
.emu | ||
.cpu | ||
.bus | ||
.get_dram_slice(return_range.clone()) | ||
.expect("unable to get memory from return range"); | ||
|
||
debug!("- Return data: {:?}", res.output); | ||
debug!("- Memory range: {:?}", return_range); | ||
debug!("- Memory size: {}", return_memory.len()); | ||
|
||
// write return data to parent's memory | ||
if res.output.len() == return_memory.len() { | ||
return_memory.copy_from_slice(&res.output); | ||
} else { | ||
warn!("Unexpected return data size!"); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can u unpack a bit what's going on here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that's what i was trying to explain in the PR description regarding r55 memory.
my understanding is that in revm we have a concept of shared memory, whereas in this case we have DRAM serving the same purpose (hence why we don't need MLOAD and MSTORE syscalls).
since each frame (call) uses an independent riscv context with it's own emulator instance, we need to keep track of the expected return data range location in DRAM (RVEmu.returned_data_destiny
).
when the parent [FRAME-0] executes a CALL
, we initialize + cache the memory range:
https://github.com/0xrusowsky/r55/blob/7e8eebd29ce061aea541fe33707b0d00c7c0900f/r55/src/exec.rs#L350-#L356
the child [FRAME-1] computes the output of the call, but is unaware of the expected return data destiny of the parent. Because of that i thought that the only place where i could "move data between frames" was in fn handle_register()
, cause that's where we manipulate the call stack >> the line where u added the comment is where we have access to both the returned data + the parent context at the same time.
posting a simplified version of the debug logs to help understand the explanation above:
// emited in `handle_register` (call stack)
=== [FRAME-0] Contract: 0x114c28ea2CDe99e41D2566B1CfAA64a84f564caf ===========
// emited in `execute_riscv` (risc-v emu)
Starting RISC-V execution: PC: 0x80300000 Return data dst: None
[Syscall::call - 0xf1]
> Return data will be written to: 2150651684..2150651716
// emited in `handle_register` (call stack)
=== [FRAME-1] Contract: 0xf6a171f57ACAc30C292e223eA8aDBb28abD3E14d ===========
// emited in `execute_riscv` (risc-v emu)
Starting RISC-V execution: PC: 0x80300000 Return data dst: None
[Syscall::return - 0xf3]
// emited in `handle_register` (call stack) >>> this is the step where i copy the output data to the expected location by the parent frame
=== [FRAME-1] Ouput: RETURN
- Return data: 0x000000000000000000000000000000000000000000000000000000000000002a
- Memory range: 2150651684..2150651716
- Memory size: 32
// emited in `handle_register` (call stack)
=== [FRAME-0] Contract: 0x114c28ea2CDe99e41D2566B1CfAA64a84f564caf ===========
// emited in `execute_riscv` (risc-v emu)
Resuming RISC-V execution: PC: 0x80300994 Return data dst: Some(
2150651684..2150651716,
)
Loaded return data: 0x000000000000000000000000000000000000000000000000000000000000002
// emited in `handle_register` (call stack)
=== [FRAME-0] Ouput: RETURN (last)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
assuming that what i explained above makes sense... git blame was pointing to Dragan as the editor of most of the handle_register()
code, so maybe u also want him to validate that my proposed approach is sound
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
based on @leonardoalt comments it looks like my implementation only works with RISCV contracts, as we do need to use shared memory when interacting with EVM contracts
}); | ||
} | ||
// RETURN logs the gas of the whole risc-v instruction set | ||
syscall_gas!(interpreter, r55_gas); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seems fine right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when testing i made sure that i got the exact same gas costs as the original implementation.
as mentioned above, this approach fixes the scenario where there are multiple calls (execution contexts)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is awesome!
Left a few small comments.
Also can you remove the rvemu.*
files (we should maybe add them to gitignore).
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` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we need this when a RISCV contract calls an EVM contract
debug!("- Memory range: {:?}", return_range); | ||
debug!("- Memory size: {}", return_memory.len()); | ||
|
||
// write return data to parent's memory |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This only supports the case where a RISCV contract calls a RISCV contract, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, i totally missed that
this PR brings x-contract calls to r55.
interface macro
the most opinionated design choice that i made is to create a macro (inspired by the contract macro) that based on an interface (trait) generates the necessary syntactic sugar to call a contract, handling abi en/decoding and the risc-v runtime call.
as u can see in
erc20x
the user simply defines an interface as they would do in solidityhttps://github.com/0xrusowsky/r55/blob/110cc5addd481368723eb0b992f081231c2b8d7d/erc20x/src/lib.rs#L17-L20
and then they can use it by initializing with the address of the target contract:
https://github.com/0xrusowsky/r55/blob/110cc5addd481368723eb0b992f081231c2b8d7d/erc20x/src/lib.rs#L25-L29
i wanted to leverage rust's type system, so the macro forces all methods of the interface to return options (in the future it would be cool to support custom errors). As u can see in the example above, unlike with solidity, we force devs to handle potential call errors.
r55 execution
after i had the interface implementation, i had to integrate the calls with revm.
in this case i've had to touch code that was written by others beforehand, so i'll try to justify why i changed some of the stuff that had been previously done >> the main reason is that frames (how revm handles the execution context of each call) hadn't been fully taken into account.
relevant changes that i made:
syscall_gas!
macro (maybe not the best name), which allows us to simply worry about the riscv gas. Finally, since i was touching gas, i also moved away from magic numbers and started using constants.logging and tests
in order to make it work, i had to debug a lot, so i ended up having prints everywhere lol
however, i thought that some of those where quite useful to keep, specially while there is still stuff to implement. Because of that i moved from
env_logger
totracing
, and added a bunch ofdebug
logs (that won't run by default, but which are really useful when developing).you can see the debug logs by running:
PS: i have format-on-save by default, so sorry for that, i know it's annoying.