Skip to content

Commit

Permalink
feat: add support for storage caching (#1006)
Browse files Browse the repository at this point in the history
* Simple REVM test runner (#788)

* refactor: nuke `evm-adapters`

* refactor: simple revm test runner

Current features:

- Can run unit tests
- Works with both revert-type tests and DSTest-type tests
- Collects logs, albeit not for reverting tests
- Integrated with config and CLI flags

Disabled features:

- Gas reports
- Tracing
- Cheatcodes
- Fuzzing
- Log decoding
- Forking mode
- Hardhat-style `console.log`, since those require
  us to decode calls to a specific address (HH does
  not emit logs)
- The debugger

In addition to this, I've disabled some tests that
could never pass under the current circumstances,
but that should be adjusted and re-enabled when their
respective features are implemented (such as fuzz tests)

* refactor: adjust CLI to new runner API

* feat: log collector inspector

* feat: hardhat logs

* chore: lint

* refactor: extract hh log converter to helper fn

* refactor: return single test result if setup fails

* build: use upstream revm

chore: renuke `evm-adapters`

* REVM fuzzer (#789)

* REVM cheatcodes (#841)

* feat: add `InspectorStack`

Adds `InspectorStack`, an inspector that calls a stack
of other inspectors sequentially.

Closes #752

* feat: port cheatcodes to revm

* feat: port `expectCall` cheatcode

* feat: extract labels from cheatcode inspector

* feat: port `expectEmit` cheatcode

* refactor: move log decoding into `forge` crate

* chore: remove unused evm patch

* test: re-enable debug logs test

* fix: record reads on `SSTORE` ops

* refactor: rename `record` to `start_record`

* docs: clarify why `DUMMY_CALL_OUTPUT` is 320 bytes

* fix: handle `expectRevert` with no return data

* build: bump revm

* chore: remove outdated todo

* refactor: use static dispatch in `InspectorStack`

* build: use k256

* fix: make gas usage not so crazy

* feat(revm): add forking mode (#835)

* feat: copy-paste old forking provider

* feat(fork): convert to REVM traits

* chore: remove unnecessary codehash handler

* feat: impl Database for shared backend

* chore: fix tests

* chore: fmt

* fix(fork): correctly convert H256 <> U256 for storage

* refactor: separate storage from accounts in cache

* feat(fork): fetch block hashes

* chore: remove unused DB parameter

* test: add test for block hashes

* feat: add forked backend to executor builder

* feat(cli): set fork url on the executor

* refactor: move shared backend to separate file

* feat(fork): add fn for instantiating forked env

* feat(cli): allow pinning block number

* fix(fork): install missing listeners

* feat(fork): instantiate environment with forked state

* fix: use a CALLER address with maxed out balance for calls

this is required because in forking mode otherwise the account wont have enough balance
to transact

* chore: fmt

Co-authored-by: Oliver Nordbjerg <hi@notbjerg.me>

* chore: fmt

* REVM tracing and gas reports (#867)

* feat: very simple traces

* feat: creation traces

* feat: setup and revert traces

* fix: fix lib addresses

* refactor: simplify tracer inspector

* fix: fill traces in correct order

* build: bump revm

* fix: get code for newly created contracts

* refactor: unify log extraction logic

* feat: trace logs

* refactor: unify labels and names

* refactor: return string from trace

Instead of passing in an empty string we then pass
around inside the trace display logic, we just return
strings where appropriate.

* refactor: remove identified contracts

* refactor: remove unused vars

* refactor: simplify `construct_func_call`

* refactor: name special characters in traces

* refactor: rework all display logic

* feat: first pass identify/decode for traces

* refactor: move tracing to own module

* refactor: simplify `test`

* feat: traces for fuzz tests

* fix: make fuzz revert reasons less verbose

* feat: port gas reports

* refactor: small readability nits

* feat: run fuzz *and* unit tests in parallel

Previously we would run each test contract in parallel,
but within each `ContractRunner` we would run unit tests
first (in parallel) and then fuzz tests (in parallel).

* refactor: move colouring logic to its own function

* fix: test contract identification

We now include three kinds of traces that are used for
identification of contracts:

- Deployment traces: these are the initial deployments
  of the test contract and libraries
- Setup traces: these are traces of calls to the `setUp`
  function
- Execution traces: these are the traces of calls to
  the test contract itself

* fix: mark setup trace as a setup trace

* fix: get correct nonce in tracer

* fix: log extraction outside of current memory

* chore: clean up complex types

* chore: remove outdated comment

* fix: make tests compile

* fix: add missing test filter function

* feat: display full address in traces

* fix: color "new" keyword in traces

* fix: filter out `console.log` calls from traces

* chore: remove unnecessary comment

* feat: add gas cost to creation traces

* fix: properly decode outputs

* refactor: destructure `TestSetup` in test funcs

* fix: ignore address for func output decoding

* fix: fix expect emit

Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
Co-authored-by: brockelmore <brockelmore@users.noreply.github.com>

* REVM debugger (#920)

* feat: port debugger data structures

* feat: initial port of `ui` crate

* chore: add `ui` crate as a workspace member

* refactor: adjust ui contract identification

* feat: grey out 0 values in debugger memory

Closes #902

* style: minor debugger ui beautification

* feat: better stack display in debugger ui

* feat: gray out zero bytes in stack view

* feat: debugger inspector

* refactor: minor code cleanup

* feat: port `forge run`

* fix: temp fix for failing `DsTest.sol` include

* chore: fix lints

* test: adjust `forge run` tests

* refactor: use simple bool for revert checks

* chore: remove unused display impl

* chore: remove unused comment

* fix: display number of stack items in ui

* docs: prettify cli help for some commands

* feat: `forge test --debug`

* refactor: `get_create_address` util

* refactor: `InspectorData`

* docs: more detailed err for `forge test --debug`

* feat: support hardhat artifacts in `vm.getCode` (#956)

Ports #903

* REVM: FFI cheatcode updates (#955)

* feat: only strip 0x in ffi output if present

Ports #904

* Update forge/src/executor/inspector/cheatcodes/ext.rs

Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>

* REVM gas fixes (#950)

* feat: account for gas refunds

* refactor: merge `call_raw` and committing variant

* fix: actually use refund quotient

* feat: strip tx gas stipend

* fix: fix reported gas usage in debugger

* build: use upstream revm

* test: adjust `forge run` gas values in tests

* chore: remove unused copy

* chore: add note on push maths

* feat: make stipend reduction optional

* fix: remove tx stipend in `forge run`

* REVM: Pull EVM executor into own crate (#961)

* refactor: move evm executor to own crate

* refactor: `evm::executor::fuzz` -> `evm::fuzz`

* refactor: `evm::debugger` -> `evm::debug`

* test: fix multi runner test

* feat: better ux for expect revert without reason (#962)

* Cross-crate testdata (#965)

* feat: cross-crate shared testdata

* refactor: move `foundry-utils` to common tests

* fix: fix getcode test

* fix: compile once in tests

* fix: fix prank cheatcode (#973)

Correctly apply `msg.sender` prank to both transfers
and calls.

* fix: prank depth math

* test: fix lib linking test

* refactor: use revm `log` hook (#984)

* refactor: use revm `log` hook

* chore: bump revm

Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>

* test: add lil-web3 to integration tests

* test: add maple labs loans to integration tests

Closes #959

* REVM fuzz dictionary (#985)

* feat: fuzz dictionary

Co-authored-by: brockelmore <31553173+brockelmore@users.noreply.github.com>

* fix: handle malformed bytecode

* fix: limit search for push bytes

* feat: collect fuzz state from logs

* feat: build initial fuzz state from db

* perf: use `Index` instead of `Selector`

Co-authored-by: brockelmore <31553173+brockelmore@users.noreply.github.com>

* feat(config): add caching settings

* feat: add none option

* feat: add foundry data dir

* feat: add storage map support

* bump ethers

* chore(clippy): make clippy happy

* refactor: diskmap

* feat: add rpc caching support

* feat: add no storage cache option

* refactor: rename cnfig value

* docs: more storage caching docs

* fix: with config builder function

* refactor: address review

Co-authored-by: Bjerg <onbjerg@users.noreply.github.com>
Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
Co-authored-by: Oliver Nordbjerg <hi@notbjerg.me>
Co-authored-by: brockelmore <brockelmore@users.noreply.github.com>
Co-authored-by: brockelmore <31553173+brockelmore@users.noreply.github.com>
  • Loading branch information
6 people committed Mar 22, 2022
1 parent f3f43ad commit 89459a6
Show file tree
Hide file tree
Showing 18 changed files with 833 additions and 124 deletions.
119 changes: 60 additions & 59 deletions Cargo.lock

Large diffs are not rendered by default.

10 changes: 4 additions & 6 deletions cli/src/cmd/forge/run.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::{
cmd::{compile_files, forge::build::BuildArgs, Cmd},
opts::evm::EvmArgs,
utils,
};
use ansi_term::Colour;
use clap::{Parser, ValueHint};
Expand All @@ -17,7 +18,7 @@ use forge::{
decode::decode_console_logs,
executor::{
opts::EvmOpts, CallResult, DatabaseRef, DeployResult, EvmError, Executor, ExecutorBuilder,
Fork, RawCallResult,
RawCallResult,
},
trace::{identifier::LocalTraceIdentifier, CallTraceArena, CallTraceDecoder, TraceKind},
CALLER,
Expand Down Expand Up @@ -98,11 +99,8 @@ impl Cmd for RunArgs {
let mut builder = ExecutorBuilder::new()
.with_cheatcodes(evm_opts.ffi)
.with_config(evm_opts.evm_env())
.with_spec(crate::utils::evm_spec(&config.evm_version));
if let Some(ref url) = self.evm_opts.fork_url {
let fork = Fork { url: url.clone(), pin_block: self.evm_opts.fork_block_number };
builder = builder.with_fork(fork);
}
.with_spec(crate::utils::evm_spec(&config.evm_version))
.with_fork(utils::get_fork(&evm_opts, &config.rpc_storage_caching));
if verbosity >= 3 {
builder = builder.with_tracing();
}
Expand Down
3 changes: 2 additions & 1 deletion cli/src/cmd/forge/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ impl Cmd for TestArgs {

// Set up the project
let project = config.project()?;
let output = super::super::compile(&project, false, false)?;
let output = crate::cmd::compile(&project, false, false)?;

// Determine print verbosity and executor verbosity
let verbosity = evm_opts.verbosity;
Expand All @@ -192,6 +192,7 @@ impl Cmd for TestArgs {
.initial_balance(evm_opts.initial_balance)
.evm_spec(evm_spec)
.sender(evm_opts.sender)
.with_fork(utils::get_fork(&evm_opts, &config.rpc_storage_caching))
.build(output, evm_opts)?;

if self.debug.is_some() {
Expand Down
16 changes: 16 additions & 0 deletions cli/src/opts/evm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@ pub struct EvmArgs {
#[serde(skip_serializing_if = "Option::is_none")]
pub fork_block_number: Option<u64>,

/// Disables storage caching entirely. This overrides any settings made in
/// [foundry_config::caching::StorageCachingConfig]
///
/// See --fork-url.
#[clap(
long,
requires = "fork-url",
help = "Explicitly disables the use of storage. All storage slots are read entirely from the endpoint."
)]
#[serde(skip)]
pub no_storage_caching: bool,

/// The initial balance of deployed test contracts.
#[clap(long)]
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down Expand Up @@ -102,6 +114,10 @@ impl Provider for EvmArgs {
dict.insert("ffi".to_string(), self.ffi.into());
}

if self.no_storage_caching {
dict.insert("no_storage_caching".to_string(), self.no_storage_caching.into());
}

Ok(Map::from([(Config::selected_profile(), dict)]))
}
}
Expand Down
77 changes: 72 additions & 5 deletions cli/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
use std::{future::Future, path::Path, str::FromStr, time::Duration};

use ethers::{solc::EvmVersion, types::U256};

use forge::executor::SpecId;
use std::{
future::Future,
path::{Path, PathBuf},
str::FromStr,
time::Duration,
};

use ethers::{
providers::{Middleware, Provider},
solc::EvmVersion,
types::U256,
};
use forge::executor::{opts::EvmOpts, Fork, SpecId};
use foundry_config::{caching::StorageCachingConfig, Config};
// reexport all `foundry_config::utils`
#[doc(hidden)]
pub use foundry_config::utils::*;
Expand Down Expand Up @@ -141,6 +150,64 @@ pub fn block_on<F: Future>(future: F) -> F::Output {
rt.block_on(future)
}

/// Helper function that returns the [Fork] to use, if any.
///
/// storage caching for the [Fork] will be enabled if
/// - `fork_url` is present
/// - `fork_block_number` is present
/// - [StorageCachingConfig] allows the `fork_url` + chain id pair
/// - storage is allowed (`no_storage_caching = false`)
///
/// If all these criteria are met, then storage caching is enabled and storage info will be written
/// to [Config::data_dir()]/<str(chainid)>/block/storage.json
///
/// for `mainnet` and `--fork-block-number 14435000` on mac the corresponding storage cache will be
/// at `$HOME`/Library/Application Support/foundry/mainnet/14435000/storage.json`
pub fn get_fork(evm_opts: &EvmOpts, config: &StorageCachingConfig) -> Option<Fork> {
fn get_cache_storage_path(
evm_opts: &EvmOpts,
config: &StorageCachingConfig,
) -> Option<PathBuf> {
if evm_opts.no_storage_caching {
// storage caching explicitly opted out of
return None
}
let url = evm_opts.fork_url.as_ref()?;
// cache only if block explicitly pinned
let block = evm_opts.fork_block_number?;
if config.enable_for_endpoint(url) {
// also need to get the chain id to compute the cache path
let provider = Provider::try_from(url.as_str()).expect("Failed to establish provider");
match block_on(provider.get_chainid()) {
Ok(chain_id) => {
let chain_id: u64 = chain_id.try_into().ok()?;
if config.enable_for_chain_id(chain_id) {
let chain = if let Ok(chain) = ethers::types::Chain::try_from(chain_id) {
chain.to_string()
} else {
format!("{}", chain_id)
};
return Some(Config::data_dir().ok()?.join(chain).join(format!("{}", block)))
}
}
Err(err) => {
tracing::warn!("Failed to get chain id for {}: {:?}", url, err);
}
}
}

None
}

if let Some(ref url) = evm_opts.fork_url {
let cache_storage = get_cache_storage_path(evm_opts, config);
let fork = Fork { url: url.clone(), pin_block: evm_opts.fork_block_number, cache_storage };
return Some(fork)
}

None
}

/// Conditionally print a message
///
/// This macro accepts a predicate and the message to print if the predicate is tru
Expand Down
5 changes: 3 additions & 2 deletions cli/tests/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -711,15 +711,16 @@ forgetest_ignore!(can_compile_local_spells, |_: TestProject, mut cmd: TestComman
let dss_exec_lib = "src/DssSpell.sol:DssExecLib:0xfD88CeE74f7D78697775aBDAE53f9Da1559728E4";

cmd.args([
"build",
"test",
"--root",
root.as_str(),
"--fork-url",
eth_rpc_url.as_str(),
"--fork-block-number",
"14435000",
"--libraries",
dss_exec_lib,
"-vvv",
"--force",
]);
cmd.print_output();
});
1 change: 1 addition & 0 deletions config/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ eyre = "0.6.5"
ethers-core = { git = "https://github.com/gakonst/ethers-rs", default-features = false }
ethers-solc = { git = "https://github.com/gakonst/ethers-rs", default-features = false, features = ["async", "svm-solc"] }
Inflector = "0.11.4"
regex = "1.5.5"

[dev-dependencies]
pretty_assertions = "1.0.0"
Expand Down
7 changes: 7 additions & 0 deletions config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ block_base_fee_per_gas = 0
block_coinbase = '0x0000000000000000000000000000000000000000'
block_timestamp = 0
block_difficulty = 0
# caches storage retrieved locally for certain chains and endpoints
# can also be restrictied to `chains = ["optimism", "mainnet"]`
# by default only remote endpoints will be cached (no `localhost` or `127.0.0.1`)
# to disable storage caching entirely set `no_storage_caching = true`
rpc_storage_caching = { chains = "all", endpoints = "remote"}
# this overrides `rpc_storage_caching` entirely
no_storage_caching = false
```

##### Additional Optimizer settings
Expand Down

0 comments on commit 89459a6

Please sign in to comment.