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

feat: Call revm directly without Foundry #13

Merged
merged 34 commits into from
Apr 14, 2024

Conversation

DanielSchiavini
Copy link
Contributor

@DanielSchiavini DanielSchiavini commented Mar 18, 2024

  • Get rid of foundry
  • Use revm directly, instead
  • Allow using the journal manually to load and commit

@DanielSchiavini DanielSchiavini marked this pull request as ready for review March 18, 2024 13:11
Copy link
Member

@DaniPopes DaniPopes left a 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

src/evm.rs Outdated Show resolved Hide resolved
@DanielSchiavini DanielSchiavini marked this pull request as draft March 18, 2024 15:51
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(
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 it will probably be cleaner do have two different functions call and create instead of using the tx.transact_to to parametrize behavior

Copy link
Collaborator

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 🤔

Copy link
Member

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

Copy link
Contributor Author

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link

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/database.rs Outdated Show resolved Hide resolved
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(
Copy link
Member

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

src/types/execution_result.rs Outdated Show resolved Hide resolved
@DanielSchiavini DanielSchiavini marked this pull request as ready for review March 21, 2024 15:59
Copy link

@rakita rakita left a 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,
Copy link

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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?
Copy link

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

Copy link
Contributor Author

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

pub journal_i: usize,
}

impl Hash for JournalCheckpoint {
Copy link

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());
Copy link

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

Copy link
Contributor Author

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);
Copy link

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

Copy link
Contributor Author

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_);
Copy link

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.

Copy link
Contributor Author

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)?;
Copy link

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?

Copy link
Contributor Author

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(
Copy link

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

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

Copy link
Contributor Author

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.
Copy link

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

Copy link
Contributor Author

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?

Copy link
Member

@DaniPopes DaniPopes left a 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

Copy link
Member

@gakonst gakonst left a 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")
Copy link
Member

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)
Copy link
Member

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>>>;
Copy link
Member

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

Copy link
Contributor Author

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?

Comment on lines +8 to +13
/// 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);
Copy link
Member

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

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

Comment on lines +26 to +36
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)
Copy link
Member

Choose a reason for hiding this comment

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

Nice!

Copy link
Collaborator

@charles-cooper charles-cooper left a 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`
@charles-cooper charles-cooper merged commit 3ffe1f9 into paradigmxyz:master Apr 14, 2024
9 of 13 checks passed
@DanielSchiavini DanielSchiavini deleted the revm branch April 15, 2024 07:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants