-
Notifications
You must be signed in to change notification settings - Fork 27
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: Call revm directly without Foundry #13
Conversation
DanielSchiavini
commented
Mar 18, 2024
•
edited
Loading
edited
- Get rid of foundry
- Use revm directly, instead
- Allow using the journal manually to load and commit
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.
Looks good, just questions
pyo3 will not accept lifetime/generic parameters so a lot of boilerplate code is needed.
src/executor.rs
Outdated
let exec = evm.handler.execution(); | ||
// call inner handling of call/create | ||
let first_frame_or_result = match ctx.evm.env.tx.transact_to { | ||
TransactTo::Call(_) => exec.call( |
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 think it will probably be cleaner do have two different functions call
and create
instead of using the tx.transact_to
to parametrize behavior
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.
but, maybe it's cleaner this way. i'm not sure 🤔
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'm assuming this is mostly copied from revm, is there a reason for this? Also cc @rakita
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.
@charles-cooper
TxEnv is from revm I don't think we can fix this here?
@DaniPopes
I am not sure where my comments went but we need to document this. I basically removed the journal finalization to keep the transaction open
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.
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.
In Evm sense, this is how call/create are specified and are differentiated by that field, so it is best to stay like that. There is one TX type that can be CALL or CREATE.
src/executor.rs
Outdated
let exec = evm.handler.execution(); | ||
// call inner handling of call/create | ||
let first_frame_or_result = match ctx.evm.env.tx.transact_to { | ||
TransactTo::Call(_) => exec.call( |
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'm assuming this is mostly copied from revm, is there a reason for this? Also cc @rakita
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.
Very creative usage! Left few comments
pyrevm.pyi
Outdated
|
||
class EVM: | ||
def __new__( | ||
cls: Type["EVM"], | ||
env: Optional[Env] = None, | ||
fork_url: Optional[str] = None, | ||
fork_block_number: Optional[int] = None, | ||
fork_block_number: Optional[str] = None, |
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.
Currious, why is this str?
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.
it's either a hash or a number
https://github.com/gakonst/ethers-rs/blob/5394d899adca736a602e316e6f0c06fdb5aa64b9/ethers-core/src/types/block.rs#L579
I'll add more documentation
src/database.rs
Outdated
let provider = Provider::<Http>::try_from(fork_url).map_err(pyerr)?; | ||
let block = fork_block_number.map(|n| BlockId::from_str(n)).map_or(Ok(None), |v| v.map(Some)).map_err(pyerr)?; | ||
let db = EthersDB::new(Arc::new(provider), block).unwrap(); | ||
// todo: do we need to get the blockEnv from the client? |
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.
Something like this is maybe needed for fetch block: https://github.com/bluealloy/revm/blob/main/crates/revm/src/db/ethersdb.rs#L23
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.
block_number
is private on ethersdb, I don't think we need to retrieve it here again
src/types/checkpoint.rs
Outdated
pub journal_i: usize, | ||
} | ||
|
||
impl Hash for JournalCheckpoint { |
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.
Should add Hash to the
JournalCheckpoint`
src/evm.rs
Outdated
}; | ||
self.checkpoints.insert(checkpoint, self.context.journaled_state.checkpoint()); |
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.
Checkpoint increments depth, this should be decoupled in Revm. and move depth from JournaledState
to EvmContext
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 would be great, but that's a whole separate task
src/evm.rs
Outdated
evm_opts, | ||
}) | ||
if let Some(revm_checkpoint) = self.checkpoints.remove(&checkpoint) { | ||
self.context.journaled_state.checkpoint_revert(revm_checkpoint); |
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.
Depth is a problem 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.
How come? The depth is increased when we create the checkpoint:
https://github.com/gakonst/pyrevm/blob/5bbc43d68046969fc598dced4d10256a4abd7d24/src/evm.rs#L80
account.clone() | ||
}; | ||
self.context.journaled_state.state.insert(address_, account); | ||
self.context.journaled_state.touch(&address_); |
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.
Set balance and touch are different operations. Balance change will not get reverted but it needs to be touched so change is noticed.
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'm not sure how to change the code based on your remark 🤔
fn get_balance(_self: PyRef<'_, Self>, address: &str) -> PyResult<U256> { | ||
let balance = _self.0.get_balance(addr(address)?).map_err(pyerr)?; | ||
fn get_balance(&mut self, address: &str) -> PyResult<U256> { | ||
let (balance, _) = self.context.balance(addr(address)?).map_err(pyerr)?; |
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.
Are we okay to Error on account that are inside Evm?
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.
Why is this going to error? The context.balance
function seems to look first in the journaled state and then the db 🤔
src/executor.rs
Outdated
let exec = evm.handler.execution(); | ||
// call inner handling of call/create | ||
let first_frame_or_result = match ctx.evm.env.tx.transact_to { | ||
TransactTo::Call(_) => exec.call( |
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.
In Evm sense, this is how call/create are specified and are differentiated by that field, so it is best to stay like that. There is one TX type that can be CALL or CREATE.
src/executor.rs
Outdated
} | ||
|
||
/// Calls the given evm. This is originally a copy of revm::Evm::transact, but it calls our own output function | ||
fn run_evm<EXT>(mut evm: Evm<'_, EXT, DB>, is_static: bool) -> PyResult<(ExecutionResult, EvmContext<DB>)> { |
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.
Didn't think that people were going to use handlers in this creative way! lol
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.
lol, creative often means there's a simpler way to do it? It's my first rust PR so please bear with me 😆
|
||
/// Returns the output of the transaction. | ||
/// This is mostly copied from revm::handler::mainnet::post_execution::output | ||
/// However, we removed the journal finalization to keep the transaction open. |
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 can be good feature set for Revm to have journaling open across transactions. Would need additional work inside JournalState to support it.
There are few unwraps()
that could panic if revert is done on unknown State: https://github.com/bluealloy/revm/blob/main/crates/revm/src/journaled_state.rs
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 solution seems to be working in our tests. Or are there other issues I'm overseeing?
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.
LGTM
I pushed a few changes to enable more checks in CI and fixing them
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.
Supportive of the idea - in general the Foundry forking backend is very powerful because it dedups requests when forking in parallel across many evm instances that reuse it.
pytest/test.py
Outdated
address, AccountInfo(code=load_contract_bin("full_math.bin")) | ||
) | ||
def test_balances_fork(): | ||
evm = EVM(fork_url=fork_url, fork_block_number="0x3b01f793ed1923cd82df5fe345b3e12211aedd514c8546e69efd6386dc0c9a97") |
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.
nit rename param to fork_block since you're passing a hash
type ForkDB = CacheDB<EthersDB<Provider<Http>>>; | ||
|
||
/// A wrapper around the `CacheDB` and `EthersDB` to provide a common interface | ||
/// without needing dynamic lifetime and generic parameters (unsupported in PyO3) |
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.
Ah didn't realize- makes sense.
use std::sync::Arc; | ||
|
||
type MemDB = CacheDB<EmptyDBWrapper>; | ||
type ForkDB = CacheDB<EthersDB<Provider<Http>>>; |
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.
Follow up move to Alloy
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.
what do you mean exactly?
/// An empty database that always returns default values when queried. | ||
/// This will also _always_ return `Some(AccountInfo)`. | ||
/// | ||
/// Copied from Foundry: <https://github.com/foundry-rs/foundry/blob/9e3ab9b3aff21c6e5ef/crates/evm/core/src/backend/in_memory_db.rs#L83-L92> | ||
#[derive(Clone, Debug, Default)] | ||
pub(crate) struct EmptyDBWrapper(EmptyDB); |
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.
Wonder if this is debt but yeah we can do on follow up
} | ||
|
||
fn snapshot(&mut self) -> PyResult<JournalCheckpoint> { | ||
let checkpoint = JournalCheckpoint { |
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.
Defer to Dragan for using this part of the api
let evm = Evm::builder() | ||
.with_context_with_handler_cfg(ContextWithHandlerCfg { | ||
cfg: handler_cfg, | ||
context: Context { | ||
evm: evm_context, | ||
external: tracer, | ||
}, | ||
}) | ||
.append_handler_register(inspector_handle_register) | ||
.build(); | ||
run_evm(evm, is_static) |
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.
Nice!
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.
hope we can get some more feedback from @rakita later but merging so we can cut a release
also ran `cargo update`