Compare commits

...

27 Commits

Author SHA1 Message Date
Omar Abdulla 1d5d4d14bf Edit how CLI reporter prints 2025-08-25 20:18:48 +03:00
Omar Abdulla f94faa2de2 Fix the logic for finding the ABI in resolc 2025-08-25 19:55:57 +03:00
Omar Abdulla 59f439b5f8 Update the kitchensink tests 2025-08-25 18:12:01 +03:00
Omar Abdulla 8d1523fd77 Configure kitchensink to use devnode by default 2025-08-25 17:43:52 +03:00
Omar d93824d973 Updated Reporting Infrastructure (#151)
* Remove the old reporting infra

* Use the Test struct more in the code

* Implement the initial set of reporter events

* Add more runner events to the reporter and refine the structure

* Add reporting infra for reporting ignored tests

* Update report to use better map data structures

* Add case status information to the report

* Integrate the reporting infrastructure with the
CLI reporter used by the program.

* Include contract compilation information in report

* Cleanup report model

* Add information on the deployed contracts
2025-08-25 11:16:09 +00:00
Omar bec5a7e390 Increase Kitchensink maximum http connections (#148)
* Throttle the Kitchensink requests

* Increase max connections limit for kitchensink
2025-08-20 22:25:17 +00:00
Omar 85033cfead Update the readme (#145) 2025-08-19 17:41:26 +00:00
Omar 76d6a154c1 Fix concurrency issues (#142)
* Fix the OS FD error

* Cache the compiler versions

* Allow for auto display impl in declare wrapper type macro

* Better logging and fix concurrency issues

* Fix tests

* Format

* Make the code even more concurrent
2025-08-19 06:47:36 +00:00
Omar c58551803d Allow multiple files in corpus (#144) 2025-08-16 16:04:17 +00:00
Omar 185edcfad9 Cached compiler artifacts (#143)
* WIP compilation cache

* Implement a persistent compilation cache

* Correct the key and value encoding for the cache
2025-08-16 16:04:13 +00:00
James Wilson 09d56f5177 Redo how we parse and use modes (#125)
* WIP redo how we parse and use modes

* test expanding, too

* WIP integrate new Mode/ParsedMode into rest of code

* First pass integrated new mode bits

* fmt

* clippy

* Remove mode we no longer support from test metadata

* Address nits

* Add ability for compiler to opt out if it can't work with some Mode/version

* Elide viaIR input if compiler does not support it

* Improve test output a little; string modes and list ignored tests

* Move Mode to common crate

* constants.mod, and Display for CaseIdx to use it

* fmt

* Rename ModePipeline::E/Y

* Re-arrange Mode things; ParsedMode in format and Mode etc in common

* Move compile check to prepare_tests

* Remove now-unused deps

* clippy nits

* Update fallback tx weights to avoid out of gas errors

* Update kitchensink weights too and fmt

* Bump default geth timeout to 10s

* 30s timeout

* Improve geth stdout logging on failure

* fix line logging

* remove --networkid and arg, back to 5s timeout for geth
2025-08-16 11:38:17 +00:00
Omar a59e287fa1 Add a cached fs abstraction (#141) 2025-08-14 15:21:05 +00:00
Omar f2045db0e9 Add compiler directives to metadata (#139) 2025-08-14 07:38:56 +00:00
Omar 5a11f44673 Misc features/improvements (#138)
* Implement various needed features and improvements

* Reorder the metadata struct

* Format comments
2025-08-13 13:50:06 +00:00
James Wilson 46aea0890d Split reporter and case runner, use channels to pass test reports (#137)
* Use channels to send data to reporting thread and avoid hangs / mutex / duration. Limit max concurrent tasks to avoid too many open files

* More appropriate name for dirver/reporter task fns

* Back to parallelise individual cases, report individual cases, address grumbles

* newline before 'Failures' title in report
2025-08-13 13:10:26 +00:00
Omar 9b40c9b9e3 Add an EVM version filter (#136)
* Add an EVM version filter

* Update naming
2025-08-12 10:19:59 +00:00
Omar f67a9bf643 Refactor/ignore null values (#135)
* Skip serialization of null values

* Add support for comments in various steps
2025-08-12 08:55:21 +00:00
Omar 67d767ffde Implement storage empty assertion (#134) 2025-08-11 13:17:19 +00:00
Omar f7fbe094ec Balance assertions (#133)
* Make metadata serializable

* Refactor tests to use steps

* Add a balance assertion test step

* Test balance deserialization

* Box the test steps

* Permit size difference in step output
2025-08-11 12:11:16 +00:00
Omar 90b2dd4cfe Make metadata serializable (#132) 2025-08-10 21:57:41 +00:00
Omar 64d63ef999 Remove the provider cache (#121)
* Remove the provider cache

* Add timing information to the CLI report
2025-08-07 03:55:24 +00:00
Omar 757bfbe116 Add more resolvable variables (#120)
* Allow resolution of base fee

* Fix block difficulty resolution

* Allow for the resolution of gas price
2025-08-06 15:17:36 +00:00
Omar 8619e7feb0 Fix the transaction tracing issues (#118)
* Set the gc mode to archive in geth

* Add a maximum to the exponential backoff wait duration

* Edit the formatting of the CLI case reporter
2025-08-06 12:25:39 +00:00
Omar edba49b301 Use SolidityLang for solc downloads (#117) 2025-08-06 10:35:05 +00:00
Omar 9980926d40 Add a case ignore flag (#114)
* Added a resolver tied to a specific block

* Increase the number of private keys

* Increase kitchensink wait time to 60 seconds

* Add a case ignore flag
2025-08-04 16:40:53 +00:00
Omar ff993d44a5 Added a resolver tied to a specific block (#111)
* Added a resolver tied to a specific block

* Increase the number of private keys

* Increase kitchensink wait time to 60 seconds
2025-08-04 12:45:47 +00:00
Omar 8cbb1a9f77 Added basic console reporting (#110)
* Added basic console reporting

* Add some waiting period to the printing task

* Print to the stderr and print logs to stdout
2025-08-04 06:05:49 +00:00
53 changed files with 5731 additions and 1709 deletions
+2
View File
@@ -7,3 +7,5 @@ node_modules
# We do not want to commit any log files that we produce from running the code locally so this is # We do not want to commit any log files that we produce from running the code locally so this is
# added to the .gitignore file. # added to the .gitignore file.
*.log *.log
profile.json.gz
Generated
+646 -12
View File
File diff suppressed because it is too large Load Diff
+14 -2
View File
@@ -8,7 +8,7 @@ authors = ["Parity Technologies <admin@parity.io>"]
license = "MIT/Apache-2.0" license = "MIT/Apache-2.0"
edition = "2024" edition = "2024"
repository = "https://github.com/paritytech/revive-differential-testing.git" repository = "https://github.com/paritytech/revive-differential-testing.git"
rust-version = "1.85.0" rust-version = "1.87.0"
[workspace.dependencies] [workspace.dependencies]
revive-dt-common = { version = "0.1.0", path = "crates/common" } revive-dt-common = { version = "0.1.0", path = "crates/common" }
@@ -25,10 +25,16 @@ revive-dt-solc-binaries = { version = "0.1.0", path = "crates/solc-binaries" }
alloy-primitives = "1.2.1" alloy-primitives = "1.2.1"
alloy-sol-types = "1.2.1" alloy-sol-types = "1.2.1"
anyhow = "1.0" anyhow = "1.0"
bson = { version = "2.15.0" }
cacache = { version = "13.1.0" }
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
dashmap = { version = "6.1.0" }
foundry-compilers-artifacts = { version = "0.18.0" } foundry-compilers-artifacts = { version = "0.18.0" }
futures = { version = "0.3.31" } futures = { version = "0.3.31" }
hex = "0.4.3" hex = "0.4.3"
regex = "1"
moka = "0.12.10"
paste = "1.0.15"
reqwest = { version = "0.12.15", features = ["json"] } reqwest = { version = "0.12.15", features = ["json"] }
once_cell = "1.21" once_cell = "1.21"
semver = { version = "1.0", features = ["serde"] } semver = { version = "1.0", features = ["serde"] }
@@ -36,19 +42,23 @@ serde = { version = "1.0", default-features = false, features = ["derive"] }
serde_json = { version = "1.0", default-features = false, features = [ serde_json = { version = "1.0", default-features = false, features = [
"arbitrary_precision", "arbitrary_precision",
"std", "std",
"unbounded_depth",
] } ] }
serde_with = { version = "3.14.0" }
sha2 = { version = "0.10.9" } sha2 = { version = "0.10.9" }
sp-core = "36.1.0" sp-core = "36.1.0"
sp-runtime = "41.1.0" sp-runtime = "41.1.0"
temp-dir = { version = "0.1.16" } temp-dir = { version = "0.1.16" }
tempfile = "3.3" tempfile = "3.3"
thiserror = "2"
tokio = { version = "1.47.0", default-features = false, features = [ tokio = { version = "1.47.0", default-features = false, features = [
"rt-multi-thread", "rt-multi-thread",
"process", "process",
"rt", "rt",
] } ] }
uuid = { version = "1.8", features = ["v4"] } uuid = { version = "1.8", features = ["v4"] }
tracing = "0.1.41" tracing = { version = "0.1.41" }
tracing-appender = { version = "0.2.3" }
tracing-subscriber = { version = "0.3.19", default-features = false, features = [ tracing-subscriber = { version = "0.3.19", default-features = false, features = [
"fmt", "fmt",
"json", "json",
@@ -83,3 +93,5 @@ features = [
inherits = "release" inherits = "release"
lto = true lto = true
codegen-units = 1 codegen-units = 1
[workspace.lints.clippy]
+193 -17
View File
@@ -1,34 +1,210 @@
# revive-differential-tests <div align="center">
<h1><code>Revive Differential Tests</code></h1>
The revive differential testing framework allows to define smart contract tests in a declarative manner in order to compile and execute them against different Ethereum-compatible blockchain implmentations. This is useful to: <p>
- Analyze observable differences in contract compilation and execution across different blockchain implementations, including contract storage, account balances, transaction output and emitted events on a per-transaction base. <strong>Differential testing for Ethereum-compatible smart contract stacks</strong>
- Collect and compare benchmark metrics such as code size, gas usage or transaction throughput per seconds (TPS) of different blockchain implementations. </p>
- Ensure reproducible contract builds across multiple compiler implementations or multiple host platforms. </div>
- Implement end-to-end regression tests for Ethereum-compatible smart contract stacks.
# Declarative test format This project compiles and executes declarative smart-contract tests against multiple platforms, then compares behavior (status, return data, events, and state diffs). Today it supports:
For now, the format used to write tests is the [matter-labs era compiler format](https://github.com/matter-labs/era-compiler-tests?tab=readme-ov-file#matter-labs-simplecomplex-format). This allows us to re-use many tests from their corpora. - Geth (EVM reference implementation)
- Revive Kitchensink (Substrate-based PolkaVM + `eth-rpc` proxy)
# The `retester` utility Use it to:
The `retester` helper utilty is used to run the tests. To get an idea of what `retester` can do, please consults its command line help: - Detect observable differences between platforms (execution success, logs, state changes)
- Ensure reproducible builds across compilers/hosts
- Run end-to-end regression suites
``` This framework uses the [MatterLabs tests format](https://github.com/matter-labs/era-compiler-tests/tree/main/solidity) for declarative tests which is composed of the following:
cargo run -p revive-dt-core -- --help
- Metadata files, this is akin to a module of tests in Rust.
- Each metadata file contains multiple cases, a case is akin to a Rust test where a module can contain multiple tests.
- Each case contains multiple steps and assertions, this is akin to any Rust test that contains multiple statements.
Metadata files are JSON files, but Solidity files can also be metadata files if they include inline metadata provided as a comment at the top of the contract.
All of the steps contained within each test case are either:
- Transactions that need to be submitted and assertions to run on the submitted transactions.
- Assertions on the state of the chain (e.g., account balances, storage, etc...)
All of the transactions submitted by the this tool to the test nodes follow a similar logic to what wallets do. We first use alloy to estimate the transaction fees, then we attach that to the transaction and submit it to the node and then await the transaction receipt.
This repository contains none of the tests and only contains the testing framework or the test runner. The tests can be found in the [`resolc-compiler-tests`](https://github.com/paritytech/resolc-compiler-tests) repository which is a clone of [MatterLab's test suite](https://github.com/matter-labs/era-compiler-tests) with some modifications and adjustments made to suit our use case.
## Requirements
This section describes the required dependencies that this framework requires to run. Compiling this framework is pretty straightforward and no additional dependencies beyond what's specified in the `Cargo.toml` file should be required.
- Stable Rust
- Geth - When doing differential testing against the PVM we submit transactions to a Geth node and to Kitchensink to compare them.
- Kitchensink - When doing differential testing against the PVM we submit transactions to a Geth node and to Kitchensink to compare them.
- ETH-RPC - All communication with Kitchensink is done through the ETH RPC.
- Solc - This is actually a transitive dependency, while this tool doesn't require solc as it downloads the versions that it requires, resolc requires that Solc is installed and available in the path.
- Resolc - This is required to compile the contracts to PolkaVM bytecode.
All of the above need to be installed and available in the path in order for the tool to work.
## Running The Tool
This tool is being updated quite frequently. Therefore, it's recommended that you don't install the tool and then run it, but rather that you run it from the root of the directory using `cargo run --release`. The help command of the tool gives you all of the information you need to know about each of the options and flags that the tool offers.
```bash
$ cargo run --release -- --help
Usage: retester [OPTIONS]
Options:
-s, --solc <SOLC>
The `solc` version to use if the test didn't specify it explicitly
[default: 0.8.29]
--wasm
Use the Wasm compiler versions
-r, --resolc <RESOLC>
The path to the `resolc` executable to be tested.
By default it uses the `resolc` binary found in `$PATH`.
If `--wasm` is set, this should point to the resolc Wasm ile.
[default: resolc]
-c, --corpus <CORPUS>
A list of test corpus JSON files to be tested
-w, --workdir <WORKING_DIRECTORY>
A place to store temporary artifacts during test execution.
Creates a temporary dir if not specified.
-g, --geth <GETH>
The path to the `geth` executable.
By default it uses `geth` binary found in `$PATH`.
[default: geth]
--geth-start-timeout <GETH_START_TIMEOUT>
The maximum time in milliseconds to wait for geth to start
[default: 5000]
--genesis <GENESIS_FILE>
Configure nodes according to this genesis.json file
[default: genesis.json]
-a, --account <ACCOUNT>
The signing account private key
[default: 0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d]
--private-keys-count <PRIVATE_KEYS_TO_ADD>
This argument controls which private keys the nodes should have access to and be added to its wallet signers. With a value of N, private keys (0, N] will be added to the signer set of the node
[default: 100000]
-l, --leader <LEADER>
The differential testing leader node implementation
[default: geth]
Possible values:
- geth: The go-ethereum reference full node EVM implementation
- kitchensink: The kitchensink runtime provides the PolkaVM (PVM) based node implentation
-f, --follower <FOLLOWER>
The differential testing follower node implementation
[default: kitchensink]
Possible values:
- geth: The go-ethereum reference full node EVM implementation
- kitchensink: The kitchensink runtime provides the PolkaVM (PVM) based node implentation
--compile-only <COMPILE_ONLY>
Only compile against this testing platform (doesn't execute the tests)
Possible values:
- geth: The go-ethereum reference full node EVM implementation
- kitchensink: The kitchensink runtime provides the PolkaVM (PVM) based node implentation
--number-of-nodes <NUMBER_OF_NODES>
Determines the amount of nodes that will be spawned for each chain
[default: 1]
--number-of-threads <NUMBER_OF_THREADS>
Determines the amount of tokio worker threads that will will be used
[default: 16]
--number-concurrent-tasks <NUMBER_CONCURRENT_TASKS>
Determines the amount of concurrent tasks that will be spawned to run tests. Defaults to 10 x the number of nodes
-e, --extract-problems
Extract problems back to the test corpus
-k, --kitchensink <KITCHENSINK>
The path to the `kitchensink` executable.
By default it uses `substrate-node` binary found in `$PATH`.
[default: substrate-node]
-p, --eth_proxy <ETH_PROXY>
The path to the `eth_proxy` executable.
By default it uses `eth-rpc` binary found in `$PATH`.
[default: eth-rpc]
-i, --invalidate-compilation-cache
Controls if the compilation cache should be invalidated or not
-h, --help
Print help (see a summary with '-h')
``` ```
For example, to run the [complex Solidity tests](https://github.com/matter-labs/era-compiler-tests/tree/main/solidity/complex), define a corpus structure as follows: To run tests with this tool you need a corpus JSON file that defines the tests included in the corpus. The simplest corpus file looks like the following:
```json ```json
{ {
"name": "ML Solidity Complex", "name": "MatterLabs Solidity Simple, Complex, and Semantic Tests",
"path": "/path/to/era-compiler-tests/solidity/complex" "path": "resolc-compiler-tests/fixtures/solidity"
} }
``` ```
Assuming this to be saved in a `ml-solidity-complex.json` file, the following command will try to compile and execute the tests found inside the corpus: > [!NOTE]
> Note that the tests can be found in the [`resolc-compiler-tests`](https://github.com/paritytech/resolc-compiler-tests) repository.
The above corpus file instructs the tool to look for all of the test cases contained within all of the metadata files of the specified directory.
The simplest command to run this tool is the following:
```bash ```bash
RUST_LOG=debug cargo r --release -p revive-dt-core -- --corpus ml-solidity-complex.json RUST_LOG="info" cargo run --release -- \
--corpus path_to_your_corpus_file.json \
--workdir path_to_a_temporary_directory_to_cache_things_in \
--number-of-nodes 5 \
> logs.log \
2> output.log
```
The above command will run the tool executing every one of the tests discovered in the path specified in the corpus file. All of the logs from the execution will be persisted in the `logs.log` file and all of the output of the tool will be persisted to the `output.log` file. If all that you're looking for is to run the tool and check which tests succeeded and failed, then the `output.log` file is what you need to be looking at. However, if you're contributing the to the tool then the `logs.log` file will be very valuable.
If you only want to run a subset of tests, then you can specify that in your corpus file. The following is an example:
```json
{
"name": "MatterLabs Solidity Simple, Complex, and Semantic Tests",
"paths": [
"path/to/a/single/metadata/file/I/want/to/run.json",
"path/to/a/directory/to/find/all/metadata/files/within"
]
}
``` ```
+13 -2
View File
@@ -1,13 +1,24 @@
{ {
"modes": [ "modes": [
"Y >=0.8.9", "Y >=0.8.9",
"E", "E"
"I"
], ],
"cases": [ "cases": [
{ {
"name": "first", "name": "first",
"inputs": [ "inputs": [
{
"address": "0xdeadbeef00000000000000000000000000000042",
"expected_balance": "1233"
},
{
"address": "0xdeadbeef00000000000000000000000000000042",
"is_storage_empty": true
},
{
"address": "0xdeadbeef00000000000000000000000000000042",
"is_storage_empty": false
},
{ {
"instance": "WBTC_1", "instance": "WBTC_1",
"method": "#deployer", "method": "#deployer",
+1
View File
@@ -0,0 +1 @@
+7
View File
@@ -10,4 +10,11 @@ rust-version.workspace = true
[dependencies] [dependencies]
anyhow = { workspace = true } anyhow = { workspace = true }
moka = { workspace = true, features = ["sync"] }
once_cell = { workspace = true }
semver = { workspace = true } semver = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true, default-features = false, features = ["time"] }
[lints]
workspace = true
+49
View File
@@ -0,0 +1,49 @@
//! This module implements a cached file system allowing for results to be stored in-memory rather
//! rather being queried from the file system again.
use std::fs;
use std::io::{Error, Result};
use std::path::{Path, PathBuf};
use moka::sync::Cache;
use once_cell::sync::Lazy;
pub fn read(path: impl AsRef<Path>) -> Result<Vec<u8>> {
static READ_CACHE: Lazy<Cache<PathBuf, Vec<u8>>> = Lazy::new(|| Cache::new(10_000));
let path = path.as_ref().canonicalize()?;
match READ_CACHE.get(path.as_path()) {
Some(content) => Ok(content),
None => {
let content = fs::read(path.as_path())?;
READ_CACHE.insert(path, content.clone());
Ok(content)
}
}
}
pub fn read_to_string(path: impl AsRef<Path>) -> Result<String> {
let content = read(path)?;
String::from_utf8(content).map_err(|_| {
Error::new(
std::io::ErrorKind::InvalidData,
"The contents of the file are not valid UTF8",
)
})
}
pub fn read_dir(path: impl AsRef<Path>) -> Result<Box<dyn Iterator<Item = Result<PathBuf>>>> {
static READ_DIR_CACHE: Lazy<Cache<PathBuf, Vec<PathBuf>>> = Lazy::new(|| Cache::new(10_000));
let path = path.as_ref().canonicalize()?;
match READ_DIR_CACHE.get(path.as_path()) {
Some(entries) => Ok(Box::new(entries.into_iter().map(Ok)) as Box<_>),
None => {
let entries = fs::read_dir(path.as_path())?
.flat_map(|maybe_entry| maybe_entry.map(|entry| entry.path()))
.collect();
READ_DIR_CACHE.insert(path.clone(), entries);
Ok(read_dir(path).unwrap())
}
}
}
+3
View File
@@ -0,0 +1,3 @@
mod poll;
pub use poll::*;
+69
View File
@@ -0,0 +1,69 @@
use std::ops::ControlFlow;
use std::time::Duration;
use anyhow::{Result, anyhow};
const EXPONENTIAL_BACKOFF_MAX_WAIT_DURATION: Duration = Duration::from_secs(60);
/// A function that polls for a fallible future for some period of time and errors if it fails to
/// get a result after polling.
///
/// Given a future that returns a [`Result<ControlFlow<O, ()>>`], this function calls the future
/// repeatedly (with some wait period) until the future returns a [`ControlFlow::Break`] or until it
/// returns an [`Err`] in which case the function stops polling and returns the error.
///
/// If the future keeps returning [`ControlFlow::Continue`] and fails to return a [`Break`] within
/// the permitted polling duration then this function returns an [`Err`]
///
/// [`Break`]: ControlFlow::Break
/// [`Continue`]: ControlFlow::Continue
pub async fn poll<F, O>(
polling_duration: Duration,
polling_wait_behavior: PollingWaitBehavior,
mut future: impl FnMut() -> F,
) -> Result<O>
where
F: Future<Output = Result<ControlFlow<O, ()>>>,
{
let mut retries = 0;
let mut total_wait_duration = Duration::ZERO;
let max_allowed_wait_duration = polling_duration;
loop {
if total_wait_duration >= max_allowed_wait_duration {
break Err(anyhow!(
"Polling failed after {} retries and a total of {:?} of wait time",
retries,
total_wait_duration
));
}
match future().await? {
ControlFlow::Continue(()) => {
let next_wait_duration = match polling_wait_behavior {
PollingWaitBehavior::Constant(duration) => duration,
PollingWaitBehavior::ExponentialBackoff => {
Duration::from_secs(2u64.pow(retries))
.min(EXPONENTIAL_BACKOFF_MAX_WAIT_DURATION)
}
};
let next_wait_duration =
next_wait_duration.min(max_allowed_wait_duration - total_wait_duration);
total_wait_duration += next_wait_duration;
retries += 1;
tokio::time::sleep(next_wait_duration).await;
}
ControlFlow::Break(output) => {
break Ok(output);
}
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum PollingWaitBehavior {
Constant(Duration),
#[default]
ExponentialBackoff,
}
@@ -1,4 +1,8 @@
use std::{borrow::Cow, collections::HashSet, path::PathBuf}; use std::{
borrow::Cow,
collections::HashSet,
path::{Path, PathBuf},
};
/// An iterator that finds files of a certain extension in the provided directory. You can think of /// An iterator that finds files of a certain extension in the provided directory. You can think of
/// this a glob pattern similar to: `${path}/**/*.md` /// this a glob pattern similar to: `${path}/**/*.md`
@@ -15,14 +19,20 @@ pub struct FilesWithExtensionIterator {
/// this vector then they will be returned when the [`Iterator::next`] method is called. If not /// this vector then they will be returned when the [`Iterator::next`] method is called. If not
/// then we visit one of the next directories to visit. /// then we visit one of the next directories to visit.
files_matching_allowed_extensions: Vec<PathBuf>, files_matching_allowed_extensions: Vec<PathBuf>,
/// This option controls if the the cached file system should be used or not. This could be
/// better for certain cases where the entries in the directories do not change and therefore
/// caching can be used.
use_cached_fs: bool,
} }
impl FilesWithExtensionIterator { impl FilesWithExtensionIterator {
pub fn new(root_directory: PathBuf) -> Self { pub fn new(root_directory: impl AsRef<Path>) -> Self {
Self { Self {
allowed_extensions: Default::default(), allowed_extensions: Default::default(),
directories_to_search: vec![root_directory], directories_to_search: vec![root_directory.as_ref().to_path_buf()],
files_matching_allowed_extensions: Default::default(), files_matching_allowed_extensions: Default::default(),
use_cached_fs: Default::default(),
} }
} }
@@ -33,6 +43,11 @@ impl FilesWithExtensionIterator {
self.allowed_extensions.insert(allowed_extension.into()); self.allowed_extensions.insert(allowed_extension.into());
self self
} }
pub fn with_use_cached_fs(mut self, use_cached_fs: bool) -> Self {
self.use_cached_fs = use_cached_fs;
self
}
} }
impl Iterator for FilesWithExtensionIterator { impl Iterator for FilesWithExtensionIterator {
@@ -45,16 +60,19 @@ impl Iterator for FilesWithExtensionIterator {
let directory_to_search = self.directories_to_search.pop()?; let directory_to_search = self.directories_to_search.pop()?;
// Read all of the entries in the directory. If we failed to read this dir's entires then we let iterator = if self.use_cached_fs {
// elect to just ignore it and look in the next directory, we do that by calling the next let Ok(dir_entries) = crate::cached_fs::read_dir(directory_to_search.as_path()) else {
// method again on the iterator, which is an intentional decision that we made here instead return self.next();
// of panicking. };
let Ok(dir_entries) = std::fs::read_dir(directory_to_search) else { Box::new(dir_entries) as Box<dyn Iterator<Item = std::io::Result<PathBuf>>>
return self.next(); } else {
let Ok(dir_entries) = std::fs::read_dir(directory_to_search) else {
return self.next();
};
Box::new(dir_entries.map(|maybe_entry| maybe_entry.map(|entry| entry.path()))) as Box<_>
}; };
for entry in dir_entries.flatten() { for entry_path in iterator.flatten() {
let entry_path = entry.path();
if entry_path.is_dir() { if entry_path.is_dir() {
self.directories_to_search.push(entry_path) self.directories_to_search.push(entry_path)
} else if entry_path.is_file() } else if entry_path.is_file()
+2
View File
@@ -1,7 +1,9 @@
//! This crate provides common concepts, functionality, types, macros, and more that other crates in //! This crate provides common concepts, functionality, types, macros, and more that other crates in
//! the workspace can benefit from. //! the workspace can benefit from.
pub mod cached_fs;
pub mod fs; pub mod fs;
pub mod futures;
pub mod iterators; pub mod iterators;
pub mod macros; pub mod macros;
pub mod types; pub mod types;
@@ -1,3 +1,25 @@
#[macro_export]
macro_rules! impl_for_wrapper {
(Display, $ident: ident) => {
#[automatically_derived]
impl std::fmt::Display for $ident {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}
};
(FromStr, $ident: ident) => {
#[automatically_derived]
impl std::str::FromStr for $ident {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
s.parse().map(Self).map_err(Into::into)
}
}
};
}
/// Defines wrappers around types. /// Defines wrappers around types.
/// ///
/// For example, the macro invocation seen below: /// For example, the macro invocation seen below:
@@ -42,7 +64,13 @@
macro_rules! define_wrapper_type { macro_rules! define_wrapper_type {
( (
$(#[$meta: meta])* $(#[$meta: meta])*
$vis:vis struct $ident: ident($ty: ty); $vis:vis struct $ident: ident($ty: ty)
$(
impl $($trait_ident: ident),*
)?
;
) => { ) => {
$(#[$meta])* $(#[$meta])*
$vis struct $ident($ty); $vis struct $ident($ty);
@@ -98,9 +126,15 @@ macro_rules! define_wrapper_type {
value.0 value.0
} }
} }
$(
$(
$crate::macros::impl_for_wrapper!($trait_ident, $ident);
)*
)?
}; };
} }
/// Technically not needed but this allows for the macro to be found in the `macros` module of the /// Technically not needed but this allows for the macro to be found in the `macros` module of the
/// crate in addition to being found in the root of the crate. /// crate in addition to being found in the root of the crate.
pub use define_wrapper_type; pub use {define_wrapper_type, impl_for_wrapper};
+2
View File
@@ -1,3 +1,5 @@
mod mode;
mod version_or_requirement; mod version_or_requirement;
pub use mode::*;
pub use version_or_requirement::*; pub use version_or_requirement::*;
+167
View File
@@ -0,0 +1,167 @@
use crate::types::VersionOrRequirement;
use semver::Version;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use std::str::FromStr;
/// This represents a mode that a given test should be run with, if possible.
///
/// We obtain this by taking a [`ParsedMode`], which may be looser or more strict
/// in its requirements, and then expanding it out into a list of [`Mode`]s.
///
/// Use [`ParsedMode::to_test_modes()`] to do this.
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Mode {
pub pipeline: ModePipeline,
pub optimize_setting: ModeOptimizerSetting,
pub version: Option<semver::VersionReq>,
}
impl Display for Mode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.pipeline.fmt(f)?;
f.write_str(" ")?;
self.optimize_setting.fmt(f)?;
if let Some(version) = &self.version {
f.write_str(" ")?;
version.fmt(f)?;
}
Ok(())
}
}
impl Mode {
/// Return all of the available mode combinations.
pub fn all() -> impl Iterator<Item = Mode> {
ModePipeline::test_cases().flat_map(|pipeline| {
ModeOptimizerSetting::test_cases().map(move |optimize_setting| Mode {
pipeline,
optimize_setting,
version: None,
})
})
}
/// Resolves the [`Mode`]'s solidity version requirement into a [`VersionOrRequirement`] if
/// the requirement is present on the object. Otherwise, the passed default version is used.
pub fn compiler_version_to_use(&self, default: Version) -> VersionOrRequirement {
match self.version {
Some(ref requirement) => requirement.clone().into(),
None => default.into(),
}
}
}
/// What do we want the compiler to do?
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
pub enum ModePipeline {
/// Compile Solidity code via Yul IR
ViaYulIR,
/// Compile Solidity direct to assembly
ViaEVMAssembly,
}
impl FromStr for ModePipeline {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
// via Yul IR
"Y" => Ok(ModePipeline::ViaYulIR),
// Don't go via Yul IR
"E" => Ok(ModePipeline::ViaEVMAssembly),
// Anything else that we see isn't a mode at all
_ => Err(anyhow::anyhow!(
"Unsupported pipeline '{s}': expected 'Y' or 'E'"
)),
}
}
}
impl Display for ModePipeline {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ModePipeline::ViaYulIR => f.write_str("Y"),
ModePipeline::ViaEVMAssembly => f.write_str("E"),
}
}
}
impl ModePipeline {
/// Should we go via Yul IR?
pub fn via_yul_ir(&self) -> bool {
matches!(self, ModePipeline::ViaYulIR)
}
/// An iterator over the available pipelines that we'd like to test,
/// when an explicit pipeline was not specified.
pub fn test_cases() -> impl Iterator<Item = ModePipeline> + Clone {
[ModePipeline::ViaYulIR, ModePipeline::ViaEVMAssembly].into_iter()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
pub enum ModeOptimizerSetting {
/// 0 / -: Don't apply any optimizations
M0,
/// 1: Apply less than default optimizations
M1,
/// 2: Apply the default optimizations
M2,
/// 3 / +: Apply aggressive optimizations
M3,
/// s: Optimize for size
Ms,
/// z: Aggressively optimize for size
Mz,
}
impl FromStr for ModeOptimizerSetting {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"M0" => Ok(ModeOptimizerSetting::M0),
"M1" => Ok(ModeOptimizerSetting::M1),
"M2" => Ok(ModeOptimizerSetting::M2),
"M3" => Ok(ModeOptimizerSetting::M3),
"Ms" => Ok(ModeOptimizerSetting::Ms),
"Mz" => Ok(ModeOptimizerSetting::Mz),
_ => Err(anyhow::anyhow!(
"Unsupported optimizer setting '{s}': expected 'M0', 'M1', 'M2', 'M3', 'Ms' or 'Mz'"
)),
}
}
}
impl Display for ModeOptimizerSetting {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ModeOptimizerSetting::M0 => f.write_str("M0"),
ModeOptimizerSetting::M1 => f.write_str("M1"),
ModeOptimizerSetting::M2 => f.write_str("M2"),
ModeOptimizerSetting::M3 => f.write_str("M3"),
ModeOptimizerSetting::Ms => f.write_str("Ms"),
ModeOptimizerSetting::Mz => f.write_str("Mz"),
}
}
}
impl ModeOptimizerSetting {
/// An iterator over the available optimizer settings that we'd like to test,
/// when an explicit optimizer setting was not specified.
pub fn test_cases() -> impl Iterator<Item = ModeOptimizerSetting> + Clone {
[
// No optimizations:
ModeOptimizerSetting::M0,
// Aggressive optimizations:
ModeOptimizerSetting::M3,
]
.into_iter()
}
/// Are any optimizations enabled?
pub fn optimizations_enabled(&self) -> bool {
!matches!(self, ModeOptimizerSetting::M0)
}
}
+4
View File
@@ -18,9 +18,13 @@ revive-common = { workspace = true }
alloy = { workspace = true } alloy = { workspace = true }
alloy-primitives = { workspace = true } alloy-primitives = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
dashmap = { workspace = true }
foundry-compilers-artifacts = { workspace = true } foundry-compilers-artifacts = { workspace = true }
semver = { workspace = true } semver = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
[lints]
workspace = true
+4
View File
@@ -0,0 +1,4 @@
use semver::Version;
/// This is the first version of solc that supports the `--via-ir` flag / "viaIR" input JSON.
pub const SOLC_VERSION_SUPPORTING_VIA_YUL_IR: Version = Version::new(0, 8, 13);
+52 -10
View File
@@ -3,9 +3,10 @@
//! - Polkadot revive resolc compiler //! - Polkadot revive resolc compiler
//! - Polkadot revive Wasm compiler //! - Polkadot revive Wasm compiler
mod constants;
use std::{ use std::{
collections::HashMap, collections::HashMap,
fs::read_to_string,
hash::Hash, hash::Hash,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
@@ -16,9 +17,13 @@ use semver::Version;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use revive_common::EVMVersion; use revive_common::EVMVersion;
use revive_dt_common::cached_fs::read_to_string;
use revive_dt_common::types::VersionOrRequirement; use revive_dt_common::types::VersionOrRequirement;
use revive_dt_config::Arguments; use revive_dt_config::Arguments;
// Re-export this as it's a part of the compiler interface.
pub use revive_dt_common::types::{Mode, ModeOptimizerSetting, ModePipeline};
pub mod revive_js; pub mod revive_js;
pub mod revive_resolc; pub mod revive_resolc;
pub mod solc; pub mod solc;
@@ -42,19 +47,27 @@ pub trait SolidityCompiler {
version: impl Into<VersionOrRequirement>, version: impl Into<VersionOrRequirement>,
) -> impl Future<Output = anyhow::Result<PathBuf>>; ) -> impl Future<Output = anyhow::Result<PathBuf>>;
fn version(&self) -> anyhow::Result<Version>; fn version(&self) -> impl Future<Output = anyhow::Result<Version>>;
/// Does the compiler support the provided mode and version settings?
fn supports_mode(
compiler_version: &Version,
optimize_setting: ModeOptimizerSetting,
pipeline: ModePipeline,
) -> bool;
} }
/// The generic compilation input configuration. /// The generic compilation input configuration.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompilerInput { pub struct CompilerInput {
pub enable_optimization: Option<bool>, pub pipeline: Option<ModePipeline>,
pub via_ir: Option<bool>, pub optimization: Option<ModeOptimizerSetting>,
pub evm_version: Option<EVMVersion>, pub evm_version: Option<EVMVersion>,
pub allow_paths: Vec<PathBuf>, pub allow_paths: Vec<PathBuf>,
pub base_path: Option<PathBuf>, pub base_path: Option<PathBuf>,
pub sources: HashMap<PathBuf, String>, pub sources: HashMap<PathBuf, String>,
pub libraries: HashMap<PathBuf, HashMap<String, Address>>, pub libraries: HashMap<PathBuf, HashMap<String, Address>>,
pub revert_string_handling: Option<RevertString>,
} }
/// The generic compilation output configuration. /// The generic compilation output configuration.
@@ -84,25 +97,26 @@ where
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
input: CompilerInput { input: CompilerInput {
enable_optimization: Default::default(), pipeline: Default::default(),
via_ir: Default::default(), optimization: Default::default(),
evm_version: Default::default(), evm_version: Default::default(),
allow_paths: Default::default(), allow_paths: Default::default(),
base_path: Default::default(), base_path: Default::default(),
sources: Default::default(), sources: Default::default(),
libraries: Default::default(), libraries: Default::default(),
revert_string_handling: Default::default(),
}, },
additional_options: T::Options::default(), additional_options: T::Options::default(),
} }
} }
pub fn with_optimization(mut self, value: impl Into<Option<bool>>) -> Self { pub fn with_optimization(mut self, value: impl Into<Option<ModeOptimizerSetting>>) -> Self {
self.input.enable_optimization = value.into(); self.input.optimization = value.into();
self self
} }
pub fn with_via_ir(mut self, value: impl Into<Option<bool>>) -> Self { pub fn with_pipeline(mut self, value: impl Into<Option<ModePipeline>>) -> Self {
self.input.via_ir = value.into(); self.input.pipeline = value.into();
self self
} }
@@ -142,11 +156,27 @@ where
self self
} }
pub fn with_revert_string_handling(
mut self,
revert_string_handling: impl Into<Option<RevertString>>,
) -> Self {
self.input.revert_string_handling = revert_string_handling.into();
self
}
pub fn with_additional_options(mut self, options: impl Into<T::Options>) -> Self { pub fn with_additional_options(mut self, options: impl Into<T::Options>) -> Self {
self.additional_options = options.into(); self.additional_options = options.into();
self self
} }
pub fn then(self, callback: impl FnOnce(Self) -> Self) -> Self {
callback(self)
}
pub fn try_then<E>(self, callback: impl FnOnce(Self) -> Result<Self, E>) -> Result<Self, E> {
callback(self)
}
pub async fn try_build( pub async fn try_build(
self, self,
compiler_path: impl AsRef<Path>, compiler_path: impl AsRef<Path>,
@@ -160,3 +190,15 @@ where
self.input.clone() self.input.clone()
} }
} }
/// Defines how the compiler should handle revert strings.
#[derive(
Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
)]
pub enum RevertString {
#[default]
Default,
Debug,
Strip,
VerboseDebug,
}
+104 -41
View File
@@ -4,8 +4,10 @@
use std::{ use std::{
path::PathBuf, path::PathBuf,
process::{Command, Stdio}, process::{Command, Stdio},
sync::LazyLock,
}; };
use dashmap::DashMap;
use revive_dt_common::types::VersionOrRequirement; use revive_dt_common::types::VersionOrRequirement;
use revive_dt_config::Arguments; use revive_dt_config::Arguments;
use revive_solc_json_interface::{ use revive_solc_json_interface::{
@@ -14,7 +16,7 @@ use revive_solc_json_interface::{
SolcStandardJsonOutput, SolcStandardJsonOutput,
}; };
use crate::{CompilerInput, CompilerOutput, SolidityCompiler}; use crate::{CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler};
use alloy::json_abi::JsonAbi; use alloy::json_abi::JsonAbi;
use anyhow::Context; use anyhow::Context;
@@ -39,17 +41,25 @@ impl SolidityCompiler for Resolc {
async fn build( async fn build(
&self, &self,
CompilerInput { CompilerInput {
enable_optimization, pipeline,
// Ignored and not honored since this is required for the resolc compilation. optimization,
via_ir: _via_ir,
evm_version, evm_version,
allow_paths, allow_paths,
base_path, base_path,
sources, sources,
libraries, libraries,
// TODO: this is currently not being handled since there is no way to pass it into
// resolc. So, we need to go back to this later once it's supported.
revert_string_handling: _,
}: CompilerInput, }: CompilerInput,
additional_options: Self::Options, additional_options: Self::Options,
) -> anyhow::Result<CompilerOutput> { ) -> anyhow::Result<CompilerOutput> {
if !matches!(pipeline, None | Some(ModePipeline::ViaYulIR)) {
anyhow::bail!(
"Resolc only supports the Y (via Yul IR) pipeline, but the provided pipeline is {pipeline:?}"
);
}
let input = SolcStandardJsonInput { let input = SolcStandardJsonInput {
language: SolcStandardJsonInputLanguage::Solidity, language: SolcStandardJsonInputLanguage::Solidity,
sources: sources sources: sources
@@ -78,7 +88,9 @@ impl SolidityCompiler for Resolc {
output_selection: Some(SolcStandardJsonInputSettingsSelection::new_required()), output_selection: Some(SolcStandardJsonInputSettingsSelection::new_required()),
via_ir: Some(true), via_ir: Some(true),
optimizer: SolcStandardJsonInputSettingsOptimizer::new( optimizer: SolcStandardJsonInputSettingsOptimizer::new(
enable_optimization.unwrap_or(false), optimization
.unwrap_or(ModeOptimizerSetting::M0)
.optimizations_enabled(),
None, None,
&Version::new(0, 0, 0), &Version::new(0, 0, 0),
false, false,
@@ -169,23 +181,41 @@ impl SolidityCompiler for Resolc {
.evm .evm
.and_then(|evm| evm.bytecode.clone()) .and_then(|evm| evm.bytecode.clone())
.context("Unexpected - Contract compiled with resolc has no bytecode")?; .context("Unexpected - Contract compiled with resolc has no bytecode")?;
let abi = contract_information let abi = {
.metadata let metadata = contract_information
.as_ref() .metadata
.and_then(|metadata| metadata.as_object()) .as_ref()
.and_then(|metadata| metadata.get("solc_metadata")) .context("No metadata found for the contract")?;
.and_then(|solc_metadata| solc_metadata.as_str()) let solc_metadata_str = match metadata {
.and_then(|metadata| serde_json::from_str::<serde_json::Value>(metadata).ok()) serde_json::Value::String(solc_metadata_str) => solc_metadata_str.as_str(),
.and_then(|metadata| { serde_json::Value::Object(metadata_object) => {
metadata.get("output").and_then(|output| { let solc_metadata_value = metadata_object
output .get("solc_metadata")
.get("abi") .context("Contract doesn't have a 'solc_metadata' field")?;
.and_then(|abi| serde_json::from_value::<JsonAbi>(abi.clone()).ok()) solc_metadata_value
}) .as_str()
}) .context("The 'solc_metadata' field is not a string")?
.context( }
"Unexpected - Failed to get the ABI for a contract compiled with resolc", serde_json::Value::Null
)?; | serde_json::Value::Bool(_)
| serde_json::Value::Number(_)
| serde_json::Value::Array(_) => {
anyhow::bail!("Unsupported type of metadata {metadata:?}")
}
};
let solc_metadata =
serde_json::from_str::<serde_json::Value>(solc_metadata_str).context(
"Failed to deserialize the solc_metadata as a serde_json generic value",
)?;
let output_value = solc_metadata
.get("output")
.context("solc_metadata doesn't have an output field")?;
let abi_value = output_value
.get("abi")
.context("solc_metadata output doesn't contain an abi field")?;
serde_json::from_value::<JsonAbi>(abi_value.clone())
.context("ABI found in solc_metadata output is not valid ABI")?
};
map.insert(contract_name, (bytecode.object, abi)); map.insert(contract_name, (bytecode.object, abi));
} }
} }
@@ -208,26 +238,59 @@ impl SolidityCompiler for Resolc {
Ok(PathBuf::from("resolc")) Ok(PathBuf::from("resolc"))
} }
fn version(&self) -> anyhow::Result<semver::Version> { async fn version(&self) -> anyhow::Result<semver::Version> {
// Logic for parsing the resolc version from the following string: /// This is a cache of the path of the compiler to the version number of the compiler. We
// Solidity frontend for the revive compiler version 0.3.0+commit.b238913.llvm-18.1.8 /// choose to cache the version in this way rather than through a field on the struct since
/// compiler objects are being created all the time from the path and the compiler object is
/// not reused over time.
static VERSION_CACHE: LazyLock<DashMap<PathBuf, Version>> = LazyLock::new(Default::default);
let output = Command::new(self.resolc_path.as_path()) match VERSION_CACHE.entry(self.resolc_path.clone()) {
.arg("--version") dashmap::Entry::Occupied(occupied_entry) => Ok(occupied_entry.get().clone()),
.stdout(Stdio::piped()) dashmap::Entry::Vacant(vacant_entry) => {
.spawn()? let output = Command::new(self.resolc_path.as_path())
.wait_with_output()? .arg("--version")
.stdout; .stdout(Stdio::piped())
let output = String::from_utf8_lossy(&output); .spawn()?
let version_string = output .wait_with_output()?
.split("version ") .stdout;
.nth(1)
.context("Version parsing failed")?
.split("+")
.next()
.context("Version parsing failed")?;
Version::parse(version_string).map_err(Into::into) let output = String::from_utf8_lossy(&output);
let version_string = output
.split("version ")
.nth(1)
.context("Version parsing failed")?
.split("+")
.next()
.context("Version parsing failed")?;
let version = Version::parse(version_string)?;
vacant_entry.insert(version.clone());
Ok(version)
}
}
}
fn supports_mode(
_compiler_version: &Version,
_optimize_setting: ModeOptimizerSetting,
pipeline: ModePipeline,
) -> bool {
// We only support the Y (IE compile via Yul IR) mode here, which also means that we can
// only use solc version 0.8.13 and above. We must always compile via Yul IR as resolc
// needs this to translate to LLVM IR and then RISCV.
// Note: the original implementation of this function looked like the following:
// ```
// pipeline == ModePipeline::ViaYulIR && compiler_version >= &SOLC_VERSION_SUPPORTING_VIA_YUL_IR
// ```
// However, that implementation is sadly incorrect since the version that's passed into this
// function is not the version of solc but the version of resolc. This is despite the fact
// that resolc depends on Solc for the initial Yul codegen. Therefore, we have skipped the
// version check until we do a better integrations between resolc and solc.
pipeline == ModePipeline::ViaYulIR
} }
} }
@@ -245,7 +308,7 @@ mod test {
let compiler = Resolc::new(path); let compiler = Resolc::new(path);
// Act // Act
let version = compiler.version(); let version = compiler.version().await;
// Assert // Assert
let _ = version.expect("Failed to get version"); let _ = version.expect("Failed to get version");
+96 -29
View File
@@ -4,13 +4,16 @@
use std::{ use std::{
path::PathBuf, path::PathBuf,
process::{Command, Stdio}, process::{Command, Stdio},
sync::LazyLock,
}; };
use dashmap::DashMap;
use revive_dt_common::types::VersionOrRequirement; use revive_dt_common::types::VersionOrRequirement;
use revive_dt_config::Arguments; use revive_dt_config::Arguments;
use revive_dt_solc_binaries::download_solc; use revive_dt_solc_binaries::download_solc;
use crate::{CompilerInput, CompilerOutput, SolidityCompiler}; use super::constants::SOLC_VERSION_SUPPORTING_VIA_YUL_IR;
use crate::{CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler};
use anyhow::Context; use anyhow::Context;
use foundry_compilers_artifacts::{ use foundry_compilers_artifacts::{
@@ -35,16 +38,28 @@ impl SolidityCompiler for Solc {
async fn build( async fn build(
&self, &self,
CompilerInput { CompilerInput {
enable_optimization, pipeline,
via_ir, optimization,
evm_version, evm_version,
allow_paths, allow_paths,
base_path, base_path,
sources, sources,
libraries, libraries,
revert_string_handling,
}: CompilerInput, }: CompilerInput,
_: Self::Options, _: Self::Options,
) -> anyhow::Result<CompilerOutput> { ) -> anyhow::Result<CompilerOutput> {
let compiler_supports_via_ir = self.version().await? >= SOLC_VERSION_SUPPORTING_VIA_YUL_IR;
// Be careful to entirely omit the viaIR field if the compiler does not support it,
// as it will error if you provide fields it does not know about. Because
// `supports_mode` is called prior to instantiating a compiler, we should never
// ask for something which is invalid.
let via_ir = match (pipeline, compiler_supports_via_ir) {
(pipeline, true) => pipeline.map(|p| p.via_yul_ir()),
(_pipeline, false) => None,
};
let input = SolcInput { let input = SolcInput {
language: SolcLanguage::Solidity, language: SolcLanguage::Solidity,
sources: Sources( sources: Sources(
@@ -55,7 +70,7 @@ impl SolidityCompiler for Solc {
), ),
settings: Settings { settings: Settings {
optimizer: Optimizer { optimizer: Optimizer {
enabled: enable_optimization, enabled: optimization.map(|o| o.optimizations_enabled()),
details: Some(Default::default()), details: Some(Default::default()),
..Default::default() ..Default::default()
}, },
@@ -87,6 +102,15 @@ impl SolidityCompiler for Solc {
}) })
.collect(), .collect(),
}, },
debug: revert_string_handling.map(|revert_string_handling| DebuggingSettings {
revert_strings: match revert_string_handling {
crate::RevertString::Default => Some(RevertStrings::Default),
crate::RevertString::Debug => Some(RevertStrings::Debug),
crate::RevertString::Strip => Some(RevertStrings::Strip),
crate::RevertString::VerboseDebug => Some(RevertStrings::VerboseDebug),
},
debug_info: Default::default(),
}),
..Default::default() ..Default::default()
}, },
}; };
@@ -187,30 +211,56 @@ impl SolidityCompiler for Solc {
Ok(path) Ok(path)
} }
fn version(&self) -> anyhow::Result<semver::Version> { async fn version(&self) -> anyhow::Result<semver::Version> {
// The following is the parsing code for the version from the solc version strings which /// This is a cache of the path of the compiler to the version number of the compiler. We
// look like the following: /// choose to cache the version in this way rather than through a field on the struct since
// ``` /// compiler objects are being created all the time from the path and the compiler object is
// solc, the solidity compiler commandline interface /// not reused over time.
// Version: 0.8.30+commit.73712a01.Darwin.appleclang static VERSION_CACHE: LazyLock<DashMap<PathBuf, Version>> = LazyLock::new(Default::default);
// ```
let child = Command::new(self.solc_path.as_path()) match VERSION_CACHE.entry(self.solc_path.clone()) {
.arg("--version") dashmap::Entry::Occupied(occupied_entry) => Ok(occupied_entry.get().clone()),
.stdout(Stdio::piped()) dashmap::Entry::Vacant(vacant_entry) => {
.spawn()?; // The following is the parsing code for the version from the solc version strings
let output = child.wait_with_output()?; // which look like the following:
let output = String::from_utf8_lossy(&output.stdout); // ```
let version_line = output // solc, the solidity compiler commandline interface
.split("Version: ") // Version: 0.8.30+commit.73712a01.Darwin.appleclang
.nth(1) // ```
.context("Version parsing failed")?; let child = Command::new(self.solc_path.as_path())
let version_string = version_line .arg("--version")
.split("+") .stdout(Stdio::piped())
.next() .spawn()?;
.context("Version parsing failed")?; let output = child.wait_with_output()?;
let output = String::from_utf8_lossy(&output.stdout);
let version_line = output
.split("Version: ")
.nth(1)
.context("Version parsing failed")?;
let version_string = version_line
.split("+")
.next()
.context("Version parsing failed")?;
Version::parse(version_string).map_err(Into::into) let version = Version::parse(version_string)?;
vacant_entry.insert(version.clone());
Ok(version)
}
}
}
fn supports_mode(
compiler_version: &Version,
_optimize_setting: ModeOptimizerSetting,
pipeline: ModePipeline,
) -> bool {
// solc 0.8.13 and above supports --via-ir, and less than that does not. Thus, we support mode E
// (ie no Yul IR) in either case, but only support Y (via Yul IR) if the compiler is new enough.
pipeline == ModePipeline::ViaEVMAssembly
|| (pipeline == ModePipeline::ViaYulIR
&& compiler_version >= &SOLC_VERSION_SUPPORTING_VIA_YUL_IR)
} }
} }
@@ -222,15 +272,13 @@ mod test {
async fn compiler_version_can_be_obtained() { async fn compiler_version_can_be_obtained() {
// Arrange // Arrange
let args = Arguments::default(); let args = Arguments::default();
println!("Getting compiler path");
let path = Solc::get_compiler_executable(&args, Version::new(0, 7, 6)) let path = Solc::get_compiler_executable(&args, Version::new(0, 7, 6))
.await .await
.unwrap(); .unwrap();
println!("Got compiler path");
let compiler = Solc::new(path); let compiler = Solc::new(path);
// Act // Act
let version = compiler.version(); let version = compiler.version().await;
// Assert // Assert
assert_eq!( assert_eq!(
@@ -238,4 +286,23 @@ mod test {
Version::new(0, 7, 6) Version::new(0, 7, 6)
) )
} }
#[tokio::test]
async fn compiler_version_can_be_obtained1() {
// Arrange
let args = Arguments::default();
let path = Solc::get_compiler_executable(&args, Version::new(0, 4, 21))
.await
.unwrap();
let compiler = Solc::new(path);
// Act
let version = compiler.version().await;
// Assert
assert_eq!(
version.expect("Failed to get version"),
Version::new(0, 4, 21)
)
}
} }
-1
View File
@@ -11,7 +11,6 @@ async fn contracts_can_be_compiled_with_solc() {
let compiler_path = Solc::get_compiler_executable(&args, Version::new(0, 8, 30)) let compiler_path = Solc::get_compiler_executable(&args, Version::new(0, 8, 30))
.await .await
.unwrap(); .unwrap();
println!("About to assert");
// Act // Act
let output = Compiler::<Solc>::new() let output = Compiler::<Solc>::new()
+2
View File
@@ -15,3 +15,5 @@ semver = { workspace = true }
temp-dir = { workspace = true } temp-dir = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
[lints]
workspace = true
+43 -7
View File
@@ -58,10 +58,6 @@ pub struct Arguments {
#[arg(long = "geth-start-timeout", default_value = "5000")] #[arg(long = "geth-start-timeout", default_value = "5000")]
pub geth_start_timeout: u64, pub geth_start_timeout: u64,
/// The test network chain ID.
#[arg(short, long = "network-id", default_value = "420420420")]
pub network_id: u64,
/// Configure nodes according to this genesis.json file. /// Configure nodes according to this genesis.json file.
#[arg(long = "genesis", default_value = "genesis.json")] #[arg(long = "genesis", default_value = "genesis.json")]
pub genesis_file: PathBuf, pub genesis_file: PathBuf,
@@ -77,7 +73,7 @@ pub struct Arguments {
/// This argument controls which private keys the nodes should have access to and be added to /// This argument controls which private keys the nodes should have access to and be added to
/// its wallet signers. With a value of N, private keys (0, N] will be added to the signer set /// its wallet signers. With a value of N, private keys (0, N] will be added to the signer set
/// of the node. /// of the node.
#[arg(long = "private-keys-count", default_value_t = 15_000)] #[arg(long = "private-keys-count", default_value_t = 100_000)]
pub private_keys_to_add: usize, pub private_keys_to_add: usize,
/// The differential testing leader node implementation. /// The differential testing leader node implementation.
@@ -96,10 +92,19 @@ pub struct Arguments {
#[arg(long, default_value = "1")] #[arg(long, default_value = "1")]
pub number_of_nodes: usize, pub number_of_nodes: usize,
/// Determines the amount of threads that will will be used. /// Determines the amount of tokio worker threads that will will be used.
#[arg(long, default_value = "12")] #[arg(
long,
default_value_t = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
)]
pub number_of_threads: usize, pub number_of_threads: usize,
/// Determines the amount of concurrent tasks that will be spawned to run tests. Defaults to 10 x the number of nodes.
#[arg(long)]
pub number_concurrent_tasks: Option<usize>,
/// Extract problems back to the test corpus. /// Extract problems back to the test corpus.
#[arg(short, long = "extract-problems")] #[arg(short, long = "extract-problems")]
pub extract_problems: bool, pub extract_problems: bool,
@@ -110,11 +115,35 @@ pub struct Arguments {
#[arg(short, long = "kitchensink", default_value = "substrate-node")] #[arg(short, long = "kitchensink", default_value = "substrate-node")]
pub kitchensink: PathBuf, pub kitchensink: PathBuf,
/// The path to the `revive-dev-node` executable.
///
/// By default it uses `revive-dev-node` binary found in `$PATH`.
#[arg(long = "revive-dev-node", default_value = "revive-dev-node")]
pub revive_dev_node: PathBuf,
/// By default the tool uses the revive-dev-node when it's running differential tests against
/// PolkaVM since the dev-node is much faster than kitchensink. This flag allows the caller to
/// configure the tool to use kitchensink rather than the dev-node.
#[arg(long)]
pub use_kitchensink_not_dev_node: bool,
/// The path to the `eth_proxy` executable. /// The path to the `eth_proxy` executable.
/// ///
/// By default it uses `eth-rpc` binary found in `$PATH`. /// By default it uses `eth-rpc` binary found in `$PATH`.
#[arg(short = 'p', long = "eth_proxy", default_value = "eth-rpc")] #[arg(short = 'p', long = "eth_proxy", default_value = "eth-rpc")]
pub eth_proxy: PathBuf, pub eth_proxy: PathBuf,
/// Controls if the compilation cache should be invalidated or not.
#[arg(short, long)]
pub invalidate_compilation_cache: bool,
/// Controls if the compiler input is included in the final report.
#[clap(long = "report.include-compiler-input")]
pub report_include_compiler_input: bool,
/// Controls if the compiler output is included in the final report.
#[clap(long = "report.include-compiler-output")]
pub report_include_compiler_output: bool,
} }
impl Arguments { impl Arguments {
@@ -134,6 +163,13 @@ impl Arguments {
panic!("should have a workdir configured") panic!("should have a workdir configured")
} }
/// Return the number of concurrent tasks to run. This is provided via the
/// `--number-concurrent-tasks` argument, and otherwise defaults to --number-of-nodes * 20.
pub fn number_of_concurrent_tasks(&self) -> usize {
self.number_concurrent_tasks
.unwrap_or(20 * self.number_of_nodes)
}
/// Try to parse `self.account` into a [PrivateKeySigner], /// Try to parse `self.account` into a [PrivateKeySigner],
/// panicing on error. /// panicing on error.
pub fn wallet(&self) -> EthereumWallet { pub fn wallet(&self) -> EthereumWallet {
+10
View File
@@ -23,11 +23,21 @@ revive-dt-report = { workspace = true }
alloy = { workspace = true } alloy = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
bson = { workspace = true }
cacache = { workspace = true }
clap = { workspace = true } clap = { workspace = true }
futures = { workspace = true } futures = { workspace = true }
indexmap = { workspace = true } indexmap = { workspace = true }
once_cell = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
tracing-appender = { workspace = true }
tracing-subscriber = { workspace = true } tracing-subscriber = { workspace = true }
semver = { workspace = true } semver = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
temp-dir = { workspace = true } temp-dir = { workspace = true }
tempfile = { workspace = true }
[lints]
workspace = true
+344
View File
@@ -0,0 +1,344 @@
//! A wrapper around the compiler which allows for caching of compilation artifacts so that they can
//! be reused between runs.
use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::Arc,
};
use futures::FutureExt;
use revive_dt_common::iterators::FilesWithExtensionIterator;
use revive_dt_compiler::{Compiler, CompilerInput, CompilerOutput, Mode, SolidityCompiler};
use revive_dt_config::Arguments;
use revive_dt_format::metadata::{ContractIdent, ContractInstance, Metadata};
use alloy::{hex::ToHexExt, json_abi::JsonAbi, primitives::Address};
use anyhow::{Error, Result};
use once_cell::sync::Lazy;
use semver::Version;
use serde::{Deserialize, Serialize};
use tokio::sync::{Mutex, RwLock};
use tracing::{Instrument, debug, debug_span, instrument};
use crate::Platform;
pub struct CachedCompiler(ArtifactsCache);
impl CachedCompiler {
pub async fn new(path: impl AsRef<Path>, invalidate_cache: bool) -> Result<Self> {
let mut cache = ArtifactsCache::new(path);
if invalidate_cache {
cache = cache.with_invalidated_cache().await?;
}
Ok(Self(cache))
}
/// Compiles or gets the compilation artifacts from the cache.
#[allow(clippy::too_many_arguments)]
#[instrument(
level = "debug",
skip_all,
fields(
metadata_file_path = %metadata_file_path.as_ref().display(),
%mode,
platform = P::config_id().to_string()
),
err
)]
pub async fn compile_contracts<P: Platform>(
&self,
metadata: &Metadata,
metadata_file_path: impl AsRef<Path>,
mode: &Mode,
config: &Arguments,
deployed_libraries: Option<&HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>>,
compilation_success_report_callback: impl Fn(
Version,
PathBuf,
bool,
Option<CompilerInput>,
CompilerOutput,
) + Clone,
compilation_failure_report_callback: impl Fn(
Option<Version>,
Option<PathBuf>,
Option<CompilerInput>,
String,
),
) -> Result<(CompilerOutput, Version)> {
static CACHE_KEY_LOCK: Lazy<RwLock<HashMap<CacheKey, Arc<Mutex<()>>>>> =
Lazy::new(Default::default);
let compiler_version_or_requirement = mode.compiler_version_to_use(config.solc.clone());
let compiler_path = <P::Compiler as SolidityCompiler>::get_compiler_executable(
config,
compiler_version_or_requirement,
)
.await
.inspect_err(|err| {
compilation_failure_report_callback(None, None, None, err.to_string())
})?;
let compiler_version = <P::Compiler as SolidityCompiler>::new(compiler_path.clone())
.version()
.await
.inspect_err(|err| {
compilation_failure_report_callback(
None,
Some(compiler_path.clone()),
None,
err.to_string(),
)
})?;
let cache_key = CacheKey {
platform_key: P::config_id().to_string(),
compiler_version: compiler_version.clone(),
metadata_file_path: metadata_file_path.as_ref().to_path_buf(),
solc_mode: mode.clone(),
};
let compilation_callback = || {
let compiler_path = compiler_path.clone();
let compiler_version = compiler_version.clone();
let compilation_success_report_callback = compilation_success_report_callback.clone();
async move {
compile_contracts::<P>(
metadata.directory()?,
compiler_path,
compiler_version,
metadata.files_to_compile()?,
mode,
deployed_libraries,
compilation_success_report_callback,
compilation_failure_report_callback,
)
.map(|compilation_result| compilation_result.map(CacheValue::new))
.await
}
.instrument(debug_span!(
"Running compilation for the cache key",
cache_key.platform_key = %cache_key.platform_key,
cache_key.compiler_version = %cache_key.compiler_version,
cache_key.metadata_file_path = %cache_key.metadata_file_path.display(),
cache_key.solc_mode = %cache_key.solc_mode,
))
};
let compiled_contracts = match deployed_libraries {
// If deployed libraries have been specified then we will re-compile the contract as it
// means that linking is required in this case.
Some(_) => {
debug!("Deployed libraries defined, recompilation must take place");
debug!("Cache miss");
compilation_callback().await?.compiler_output
}
// If no deployed libraries are specified then we can follow the cached flow and attempt
// to lookup the compilation artifacts in the cache.
None => {
debug!("Deployed libraries undefined, attempting to make use of cache");
// Lock this specific cache key such that we do not get inconsistent state. We want
// that when multiple cases come in asking for the compilation artifacts then they
// don't all trigger a compilation if there's a cache miss. Hence, the lock here.
let read_guard = CACHE_KEY_LOCK.read().await;
let mutex = match read_guard.get(&cache_key).cloned() {
Some(value) => value,
None => {
drop(read_guard);
CACHE_KEY_LOCK
.write()
.await
.entry(cache_key.clone())
.or_default()
.clone()
}
};
let _guard = mutex.lock().await;
match self.0.get(&cache_key).await {
Some(cache_value) => {
compilation_success_report_callback(
compiler_version.clone(),
compiler_path,
true,
None,
cache_value.compiler_output.clone(),
);
cache_value.compiler_output
}
None => compilation_callback().await?.compiler_output,
}
}
};
Ok((compiled_contracts, compiler_version))
}
}
#[allow(clippy::too_many_arguments)]
async fn compile_contracts<P: Platform>(
metadata_directory: impl AsRef<Path>,
compiler_path: impl AsRef<Path>,
compiler_version: Version,
mut files_to_compile: impl Iterator<Item = PathBuf>,
mode: &Mode,
deployed_libraries: Option<&HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>>,
compilation_success_report_callback: impl Fn(
Version,
PathBuf,
bool,
Option<CompilerInput>,
CompilerOutput,
),
compilation_failure_report_callback: impl Fn(
Option<Version>,
Option<PathBuf>,
Option<CompilerInput>,
String,
),
) -> Result<CompilerOutput> {
let all_sources_in_dir = FilesWithExtensionIterator::new(metadata_directory.as_ref())
.with_allowed_extension("sol")
.with_use_cached_fs(true)
.collect::<Vec<_>>();
let compiler = Compiler::<P::Compiler>::new()
.with_allow_path(metadata_directory)
// Handling the modes
.with_optimization(mode.optimize_setting)
.with_pipeline(mode.pipeline)
// Adding the contract sources to the compiler.
.try_then(|compiler| {
files_to_compile.try_fold(compiler, |compiler, path| compiler.with_source(path))
})
.inspect_err(|err| {
compilation_failure_report_callback(
Some(compiler_version.clone()),
Some(compiler_path.as_ref().to_path_buf()),
None,
err.to_string(),
)
})?
// Adding the deployed libraries to the compiler.
.then(|compiler| {
deployed_libraries
.iter()
.flat_map(|value| value.iter())
.map(|(instance, (ident, address, abi))| (instance, ident, address, abi))
.flat_map(|(_, ident, address, _)| {
all_sources_in_dir
.iter()
.map(move |path| (ident, address, path))
})
.fold(compiler, |compiler, (ident, address, path)| {
compiler.with_library(path, ident.as_str(), *address)
})
});
let compiler_input = compiler.input();
let compiler_output = compiler
.try_build(compiler_path.as_ref())
.await
.inspect_err(|err| {
compilation_failure_report_callback(
Some(compiler_version.clone()),
Some(compiler_path.as_ref().to_path_buf()),
Some(compiler_input.clone()),
err.to_string(),
)
})?;
compilation_success_report_callback(
compiler_version,
compiler_path.as_ref().to_path_buf(),
false,
Some(compiler_input),
compiler_output.clone(),
);
Ok(compiler_output)
}
struct ArtifactsCache {
path: PathBuf,
}
impl ArtifactsCache {
pub fn new(path: impl AsRef<Path>) -> Self {
Self {
path: path.as_ref().to_path_buf(),
}
}
#[instrument(level = "debug", skip_all, err)]
pub async fn with_invalidated_cache(self) -> Result<Self> {
cacache::clear(self.path.as_path())
.await
.map_err(Into::<Error>::into)?;
Ok(self)
}
#[instrument(level = "debug", skip_all, err)]
pub async fn insert(&self, key: &CacheKey, value: &CacheValue) -> Result<()> {
let key = bson::to_vec(key)?;
let value = bson::to_vec(value)?;
cacache::write(self.path.as_path(), key.encode_hex(), value).await?;
Ok(())
}
pub async fn get(&self, key: &CacheKey) -> Option<CacheValue> {
let key = bson::to_vec(key).ok()?;
let value = cacache::read(self.path.as_path(), key.encode_hex())
.await
.ok()?;
let value = bson::from_slice::<CacheValue>(&value).ok()?;
Some(value)
}
#[instrument(level = "debug", skip_all, err)]
pub async fn get_or_insert_with(
&self,
key: &CacheKey,
callback: impl AsyncFnOnce() -> Result<CacheValue>,
) -> Result<CacheValue> {
match self.get(key).await {
Some(value) => {
debug!("Cache hit");
Ok(value)
}
None => {
debug!("Cache miss");
let value = callback().await?;
self.insert(key, &value).await?;
Ok(value)
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct CacheKey {
/// The platform name that this artifact was compiled for. For example, this could be EVM or
/// PVM.
platform_key: String,
/// The version of the compiler that was used to compile the artifacts.
compiler_version: Version,
/// The path of the metadata file that the compilation artifacts are for.
metadata_file_path: PathBuf,
/// The mode that the compilation artifacts where compiled with.
solc_mode: Mode,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct CacheValue {
/// The compiler output from the compilation run.
compiler_output: CompilerOutput,
}
impl CacheValue {
pub fn new(compiler_output: CompilerOutput) -> Self {
Self { compiler_output }
}
}
+334 -181
View File
@@ -4,31 +4,37 @@ use std::collections::HashMap;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::path::PathBuf; use std::path::PathBuf;
use alloy::consensus::EMPTY_ROOT_HASH;
use alloy::hex;
use alloy::json_abi::JsonAbi; use alloy::json_abi::JsonAbi;
use alloy::network::{Ethereum, TransactionBuilder}; use alloy::network::{Ethereum, TransactionBuilder};
use alloy::primitives::U256; use alloy::primitives::U256;
use alloy::rpc::types::TransactionReceipt; use alloy::rpc::types::TransactionReceipt;
use alloy::rpc::types::trace::geth::{ use alloy::rpc::types::trace::geth::{
CallFrame, GethDebugBuiltInTracerType, GethDebugTracerType, GethDebugTracingOptions, GethTrace, CallFrame, GethDebugBuiltInTracerType, GethDebugTracerConfig, GethDebugTracerType,
PreStateConfig, GethDebugTracingOptions, GethTrace, PreStateConfig,
}; };
use alloy::{ use alloy::{
primitives::Address, primitives::Address,
rpc::types::{ rpc::types::{TransactionRequest, trace::geth::DiffMode},
TransactionRequest,
trace::geth::{AccountState, DiffMode},
},
}; };
use anyhow::Context; use anyhow::Context;
use futures::TryStreamExt;
use indexmap::IndexMap; use indexmap::IndexMap;
use revive_dt_format::traits::{ResolutionContext, ResolverApi};
use revive_dt_report::ExecutionSpecificReporter;
use semver::Version; use semver::Version;
use revive_dt_format::case::{Case, CaseIdx}; use revive_dt_format::case::Case;
use revive_dt_format::input::{Calldata, EtherValue, Expected, ExpectedOutput, Method}; use revive_dt_format::input::{
use revive_dt_format::metadata::{ContractInstance, ContractPathAndIdent}; BalanceAssertion, Calldata, EtherValue, Expected, ExpectedOutput, Input, Method, StepIdx,
use revive_dt_format::{input::Input, metadata::Metadata}; StorageEmptyAssertion,
use revive_dt_node::Node; };
use revive_dt_format::metadata::{ContractIdent, ContractInstance, ContractPathAndIdent};
use revive_dt_format::{input::Step, metadata::Metadata};
use revive_dt_node_interaction::EthereumNode; use revive_dt_node_interaction::EthereumNode;
use tokio::try_join;
use tracing::{Instrument, info, info_span, instrument};
use crate::Platform; use crate::Platform;
@@ -37,7 +43,7 @@ pub struct CaseState<T: Platform> {
compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>, compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
/// This map stores the contracts deployments for this case. /// This map stores the contracts deployments for this case.
deployed_contracts: HashMap<ContractInstance, (Address, JsonAbi)>, deployed_contracts: HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>,
/// This map stores the variables used for each one of the cases contained in the metadata /// This map stores the variables used for each one of the cases contained in the metadata
/// file. /// file.
@@ -46,6 +52,9 @@ pub struct CaseState<T: Platform> {
/// Stores the version used for the current case. /// Stores the version used for the current case.
compiler_version: Version, compiler_version: Version,
/// The execution reporter.
execution_reporter: ExecutionSpecificReporter,
phantom: PhantomData<T>, phantom: PhantomData<T>,
} }
@@ -56,26 +65,54 @@ where
pub fn new( pub fn new(
compiler_version: Version, compiler_version: Version,
compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>, compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
deployed_contracts: HashMap<ContractInstance, (Address, JsonAbi)>, deployed_contracts: HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>,
execution_reporter: ExecutionSpecificReporter,
) -> Self { ) -> Self {
Self { Self {
compiled_contracts, compiled_contracts,
deployed_contracts, deployed_contracts,
variables: Default::default(), variables: Default::default(),
compiler_version, compiler_version,
execution_reporter,
phantom: PhantomData, phantom: PhantomData,
} }
} }
pub async fn handle_step(
&mut self,
metadata: &Metadata,
step: &Step,
node: &T::Blockchain,
) -> anyhow::Result<StepOutput> {
match step {
Step::FunctionCall(input) => {
let (receipt, geth_trace, diff_mode) =
self.handle_input(metadata, input, node).await?;
Ok(StepOutput::FunctionCall(receipt, geth_trace, diff_mode))
}
Step::BalanceAssertion(balance_assertion) => {
self.handle_balance_assertion(metadata, balance_assertion, node)
.await?;
Ok(StepOutput::BalanceAssertion)
}
Step::StorageEmptyAssertion(storage_empty) => {
self.handle_storage_empty(metadata, storage_empty, node)
.await?;
Ok(StepOutput::StorageEmptyAssertion)
}
}
.inspect(|_| info!("Step Succeeded"))
}
#[instrument(level = "info", name = "Handling Input", skip_all)]
pub async fn handle_input( pub async fn handle_input(
&mut self, &mut self,
metadata: &Metadata, metadata: &Metadata,
case_idx: CaseIdx,
input: &Input, input: &Input,
node: &T::Blockchain, node: &T::Blockchain,
) -> anyhow::Result<(TransactionReceipt, GethTrace, DiffMode)> { ) -> anyhow::Result<(TransactionReceipt, GethTrace, DiffMode)> {
let deployment_receipts = self let deployment_receipts = self
.handle_contract_deployment(metadata, case_idx, input, node) .handle_input_contract_deployment(metadata, input, node)
.await?; .await?;
let execution_receipt = self let execution_receipt = self
.handle_input_execution(input, deployment_receipts, node) .handle_input_execution(input, deployment_receipts, node)
@@ -84,27 +121,49 @@ where
.handle_input_call_frame_tracing(&execution_receipt, node) .handle_input_call_frame_tracing(&execution_receipt, node)
.await?; .await?;
self.handle_input_variable_assignment(input, &tracing_result)?; self.handle_input_variable_assignment(input, &tracing_result)?;
self.handle_input_expectations(input, &execution_receipt, node, &tracing_result) let (_, (geth_trace, diff_mode)) = try_join!(
self.handle_input_expectations(input, &execution_receipt, node, &tracing_result),
self.handle_input_diff(&execution_receipt, node)
)?;
Ok((execution_receipt, geth_trace, diff_mode))
}
#[instrument(level = "info", name = "Handling Balance Assertion", skip_all)]
pub async fn handle_balance_assertion(
&mut self,
metadata: &Metadata,
balance_assertion: &BalanceAssertion,
node: &T::Blockchain,
) -> anyhow::Result<()> {
self.handle_balance_assertion_contract_deployment(metadata, balance_assertion, node)
.await?; .await?;
self.handle_input_diff(case_idx, execution_receipt, node) self.handle_balance_assertion_execution(balance_assertion, node)
.await .await?;
Ok(())
}
#[instrument(level = "info", name = "Handling Storage Assertion", skip_all)]
pub async fn handle_storage_empty(
&mut self,
metadata: &Metadata,
storage_empty: &StorageEmptyAssertion,
node: &T::Blockchain,
) -> anyhow::Result<()> {
self.handle_storage_empty_assertion_contract_deployment(metadata, storage_empty, node)
.await?;
self.handle_storage_empty_assertion_execution(storage_empty, node)
.await?;
Ok(())
} }
/// Handles the contract deployment for a given input performing it if it needs to be performed. /// Handles the contract deployment for a given input performing it if it needs to be performed.
async fn handle_contract_deployment( #[instrument(level = "info", skip_all)]
async fn handle_input_contract_deployment(
&mut self, &mut self,
metadata: &Metadata, metadata: &Metadata,
case_idx: CaseIdx,
input: &Input, input: &Input,
node: &T::Blockchain, node: &T::Blockchain,
) -> anyhow::Result<HashMap<ContractInstance, TransactionReceipt>> { ) -> anyhow::Result<HashMap<ContractInstance, TransactionReceipt>> {
let span = tracing::debug_span!(
"Handling contract deployment",
?case_idx,
instance = ?input.instance
);
let _guard = span.enter();
let mut instances_we_must_deploy = IndexMap::<ContractInstance, bool>::new(); let mut instances_we_must_deploy = IndexMap::<ContractInstance, bool>::new();
for instance in input.find_all_contract_instances().into_iter() { for instance in input.find_all_contract_instances().into_iter() {
if !self.deployed_contracts.contains_key(&instance) { if !self.deployed_contracts.contains_key(&instance) {
@@ -116,11 +175,6 @@ where
instances_we_must_deploy.insert(input.instance.clone(), true); instances_we_must_deploy.insert(input.instance.clone(), true);
} }
tracing::debug!(
instances_to_deploy = instances_we_must_deploy.len(),
"Computed the number of required deployments for input"
);
let mut receipts = HashMap::new(); let mut receipts = HashMap::new();
for (instance, deploy_with_constructor_arguments) in instances_we_must_deploy.into_iter() { for (instance, deploy_with_constructor_arguments) in instances_we_must_deploy.into_iter() {
let calldata = deploy_with_constructor_arguments.then_some(&input.calldata); let calldata = deploy_with_constructor_arguments.then_some(&input.calldata);
@@ -147,6 +201,7 @@ where
} }
/// Handles the execution of the input in terms of the calls that need to be made. /// Handles the execution of the input in terms of the calls that need to be made.
#[instrument(level = "info", skip_all)]
async fn handle_input_execution( async fn handle_input_execution(
&mut self, &mut self,
input: &Input, input: &Input,
@@ -161,36 +216,24 @@ where
.context("Failed to find deployment receipt"), .context("Failed to find deployment receipt"),
Method::Fallback | Method::FunctionName(_) => { Method::Fallback | Method::FunctionName(_) => {
let tx = match input let tx = match input
.legacy_transaction(&self.deployed_contracts, &self.variables, node) .legacy_transaction(node, self.default_resolution_context())
.await .await
{ {
Ok(tx) => { Ok(tx) => tx,
tracing::debug!("Legacy transaction data: {tx:#?}");
tx
}
Err(err) => { Err(err) => {
tracing::error!("Failed to construct legacy transaction: {err:?}");
return Err(err); return Err(err);
} }
}; };
tracing::trace!("Executing transaction for input: {input:?}");
match node.execute_transaction(tx).await { match node.execute_transaction(tx).await {
Ok(receipt) => Ok(receipt), Ok(receipt) => Ok(receipt),
Err(err) => { Err(err) => Err(err),
tracing::error!(
"Failed to execute transaction when executing the contract: {}, {:?}",
&*input.instance,
err
);
Err(err)
}
} }
} }
} }
} }
#[instrument(level = "info", skip_all)]
async fn handle_input_call_frame_tracing( async fn handle_input_call_frame_tracing(
&self, &self,
execution_receipt: &TransactionReceipt, execution_receipt: &TransactionReceipt,
@@ -202,6 +245,14 @@ where
tracer: Some(GethDebugTracerType::BuiltInTracer( tracer: Some(GethDebugTracerType::BuiltInTracer(
GethDebugBuiltInTracerType::CallTracer, GethDebugBuiltInTracerType::CallTracer,
)), )),
tracer_config: GethDebugTracerConfig(serde_json::json! {{
"onlyTopCall": true,
"withLog": false,
"withStorage": false,
"withMemory": false,
"withStack": false,
"withReturnData": true
}}),
..Default::default() ..Default::default()
}, },
) )
@@ -213,6 +264,7 @@ where
}) })
} }
#[instrument(level = "info", skip_all)]
fn handle_input_variable_assignment( fn handle_input_variable_assignment(
&mut self, &mut self,
input: &Input, input: &Input,
@@ -233,21 +285,24 @@ where
) { ) {
let value = U256::from_be_slice(output_word); let value = U256::from_be_slice(output_word);
self.variables.insert(variable_name.clone(), value); self.variables.insert(variable_name.clone(), value);
tracing::info!(
variable_name,
variable_value = hex::encode(value.to_be_bytes::<32>()),
"Assigned variable"
);
} }
Ok(()) Ok(())
} }
#[instrument(level = "info", skip_all)]
async fn handle_input_expectations( async fn handle_input_expectations(
&mut self, &self,
input: &Input, input: &Input,
execution_receipt: &TransactionReceipt, execution_receipt: &TransactionReceipt,
node: &T::Blockchain, resolver: &impl ResolverApi,
tracing_result: &CallFrame, tracing_result: &CallFrame,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let span = tracing::info_span!("Handling input expectations");
let _guard = span.enter();
// Resolving the `input.expected` into a series of expectations that we can then assert on. // Resolving the `input.expected` into a series of expectations that we can then assert on.
let mut expectations = match input { let mut expectations = match input {
Input { Input {
@@ -276,24 +331,25 @@ where
} }
} }
for expectation in expectations.iter() { futures::stream::iter(expectations.into_iter().map(Ok))
self.handle_input_expectation_item( .try_for_each_concurrent(None, |expectation| async move {
execution_receipt, self.handle_input_expectation_item(
node, execution_receipt,
expectation, resolver,
tracing_result, expectation,
) tracing_result,
.await?; )
} .await
})
Ok(()) .await
} }
#[instrument(level = "info", skip_all)]
async fn handle_input_expectation_item( async fn handle_input_expectation_item(
&mut self, &self,
execution_receipt: &TransactionReceipt, execution_receipt: &TransactionReceipt,
node: &T::Blockchain, resolver: &impl ResolverApi,
expectation: &ExpectedOutput, expectation: ExpectedOutput,
tracing_result: &CallFrame, tracing_result: &CallFrame,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
if let Some(ref version_requirement) = expectation.compiler_version { if let Some(ref version_requirement) = expectation.compiler_version {
@@ -302,9 +358,10 @@ where
} }
} }
let deployed_contracts = &mut self.deployed_contracts; let resolution_context = self
let variables = &mut self.variables; .default_resolution_context()
let chain_state_provider = node; .with_block_number(execution_receipt.block_number.as_ref())
.with_transaction_hash(&execution_receipt.transaction_hash);
// Handling the receipt state assertion. // Handling the receipt state assertion.
let expected = !expectation.exception; let expected = !expectation.exception;
@@ -327,12 +384,7 @@ where
let expected = expected_calldata; let expected = expected_calldata;
let actual = &tracing_result.output.as_ref().unwrap_or_default(); let actual = &tracing_result.output.as_ref().unwrap_or_default();
if !expected if !expected
.is_equivalent( .is_equivalent(actual, resolver, resolution_context)
actual,
deployed_contracts,
&*variables,
chain_state_provider,
)
.await? .await?
{ {
tracing::error!( tracing::error!(
@@ -358,14 +410,16 @@ where
} }
// Handling the events assertion. // Handling the events assertion.
for (expected_event, actual_event) in for (event_idx, (expected_event, actual_event)) in expected_events
expected_events.iter().zip(execution_receipt.logs()) .iter()
.zip(execution_receipt.logs())
.enumerate()
{ {
// Handling the emitter assertion. // Handling the emitter assertion.
if let Some(ref expected_address) = expected_event.address { if let Some(ref expected_address) = expected_event.address {
let expected = Address::from_slice( let expected = Address::from_slice(
Calldata::new_compound([expected_address]) Calldata::new_compound([expected_address])
.calldata(deployed_contracts, &*variables, node) .calldata(resolver, resolution_context)
.await? .await?
.get(12..32) .get(12..32)
.expect("Can't fail"), .expect("Can't fail"),
@@ -373,6 +427,7 @@ where
let actual = actual_event.address(); let actual = actual_event.address();
if actual != expected { if actual != expected {
tracing::error!( tracing::error!(
event_idx,
%expected, %expected,
%actual, %actual,
"Event emitter assertion failed", "Event emitter assertion failed",
@@ -392,15 +447,11 @@ where
{ {
let expected = Calldata::new_compound([expected]); let expected = Calldata::new_compound([expected]);
if !expected if !expected
.is_equivalent( .is_equivalent(&actual.0, resolver, resolution_context)
&actual.0,
deployed_contracts,
&*variables,
chain_state_provider,
)
.await? .await?
{ {
tracing::error!( tracing::error!(
event_idx,
?execution_receipt, ?execution_receipt,
?expected, ?expected,
?actual, ?actual,
@@ -416,15 +467,11 @@ where
let expected = &expected_event.values; let expected = &expected_event.values;
let actual = &actual_event.data().data; let actual = &actual_event.data().data;
if !expected if !expected
.is_equivalent( .is_equivalent(&actual.0, resolver, resolution_context)
&actual.0,
deployed_contracts,
&*variables,
chain_state_provider,
)
.await? .await?
{ {
tracing::error!( tracing::error!(
event_idx,
?execution_receipt, ?execution_receipt,
?expected, ?expected,
?actual, ?actual,
@@ -440,15 +487,12 @@ where
Ok(()) Ok(())
} }
#[instrument(level = "info", skip_all)]
async fn handle_input_diff( async fn handle_input_diff(
&mut self, &self,
_: CaseIdx, execution_receipt: &TransactionReceipt,
execution_receipt: TransactionReceipt,
node: &T::Blockchain, node: &T::Blockchain,
) -> anyhow::Result<(TransactionReceipt, GethTrace, DiffMode)> { ) -> anyhow::Result<(GethTrace, DiffMode)> {
let span = tracing::info_span!("Handling input diff");
let _guard = span.enter();
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig { let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
diff_mode: Some(true), diff_mode: Some(true),
disable_code: None, disable_code: None,
@@ -456,11 +500,137 @@ where
}); });
let trace = node let trace = node
.trace_transaction(&execution_receipt, trace_options) .trace_transaction(execution_receipt, trace_options)
.await?; .await?;
let diff = node.state_diff(&execution_receipt).await?; let diff = node.state_diff(execution_receipt).await?;
Ok((execution_receipt, trace, diff)) Ok((trace, diff))
}
#[instrument(level = "info", skip_all)]
pub async fn handle_balance_assertion_contract_deployment(
&mut self,
metadata: &Metadata,
balance_assertion: &BalanceAssertion,
node: &T::Blockchain,
) -> anyhow::Result<()> {
let Some(instance) = balance_assertion
.address
.strip_suffix(".address")
.map(ContractInstance::new)
else {
return Ok(());
};
self.get_or_deploy_contract_instance(
&instance,
metadata,
Input::default_caller(),
None,
None,
node,
)
.await?;
Ok(())
}
#[instrument(level = "info", skip_all)]
pub async fn handle_balance_assertion_execution(
&mut self,
BalanceAssertion {
address: address_string,
expected_balance: amount,
..
}: &BalanceAssertion,
node: &T::Blockchain,
) -> anyhow::Result<()> {
let address = Address::from_slice(
Calldata::new_compound([address_string])
.calldata(node, self.default_resolution_context())
.await?
.get(12..32)
.expect("Can't fail"),
);
let balance = node.balance_of(address).await?;
let expected = *amount;
let actual = balance;
if expected != actual {
tracing::error!(%expected, %actual, %address, "Balance assertion failed");
anyhow::bail!(
"Balance assertion failed - Expected {} but got {} for {} resolved to {}",
expected,
actual,
address_string,
address,
)
}
Ok(())
}
#[instrument(level = "info", skip_all)]
pub async fn handle_storage_empty_assertion_contract_deployment(
&mut self,
metadata: &Metadata,
storage_empty_assertion: &StorageEmptyAssertion,
node: &T::Blockchain,
) -> anyhow::Result<()> {
let Some(instance) = storage_empty_assertion
.address
.strip_suffix(".address")
.map(ContractInstance::new)
else {
return Ok(());
};
self.get_or_deploy_contract_instance(
&instance,
metadata,
Input::default_caller(),
None,
None,
node,
)
.await?;
Ok(())
}
#[instrument(level = "info", skip_all)]
pub async fn handle_storage_empty_assertion_execution(
&mut self,
StorageEmptyAssertion {
address: address_string,
is_storage_empty,
..
}: &StorageEmptyAssertion,
node: &T::Blockchain,
) -> anyhow::Result<()> {
let address = Address::from_slice(
Calldata::new_compound([address_string])
.calldata(node, self.default_resolution_context())
.await?
.get(12..32)
.expect("Can't fail"),
);
let storage = node.latest_state_proof(address, Default::default()).await?;
let is_empty = storage.storage_hash == EMPTY_ROOT_HASH;
let expected = is_storage_empty;
let actual = is_empty;
if *expected != actual {
tracing::error!(%expected, %actual, %address, "Storage Empty Assertion failed");
anyhow::bail!(
"Storage Empty Assertion failed - Expected {} but got {} for {} resolved to {}",
expected,
actual,
address_string,
address,
)
};
Ok(())
} }
/// Gets the information of a deployed contract or library from the state. If it's found to not /// Gets the information of a deployed contract or library from the state. If it's found to not
@@ -478,7 +648,7 @@ where
value: Option<EtherValue>, value: Option<EtherValue>,
node: &T::Blockchain, node: &T::Blockchain,
) -> anyhow::Result<(Address, JsonAbi, Option<TransactionReceipt>)> { ) -> anyhow::Result<(Address, JsonAbi, Option<TransactionReceipt>)> {
if let Some((address, abi)) = self.deployed_contracts.get(contract_instance) { if let Some((_, address, abi)) = self.deployed_contracts.get(contract_instance) {
return Ok((*address, abi.clone(), None)); return Ok((*address, abi.clone(), None));
} }
@@ -487,7 +657,6 @@ where
contract_ident, contract_ident,
}) = metadata.contract_sources()?.remove(contract_instance) }) = metadata.contract_sources()?.remove(contract_instance)
else { else {
tracing::error!("Contract source not found for instance");
anyhow::bail!( anyhow::bail!(
"Contract source not found for instance {:?}", "Contract source not found for instance {:?}",
contract_instance contract_instance
@@ -500,11 +669,6 @@ where
.and_then(|source_file_contracts| source_file_contracts.get(contract_ident.as_ref())) .and_then(|source_file_contracts| source_file_contracts.get(contract_ident.as_ref()))
.cloned() .cloned()
else { else {
tracing::error!(
contract_source_path = contract_source_path.display().to_string(),
contract_ident = contract_ident.as_ref(),
"Failed to find information for contract"
);
anyhow::bail!( anyhow::bail!(
"Failed to find information for contract {:?}", "Failed to find information for contract {:?}",
contract_instance contract_instance
@@ -526,7 +690,7 @@ where
if let Some(calldata) = calldata { if let Some(calldata) = calldata {
let calldata = calldata let calldata = calldata
.calldata(&self.deployed_contracts, None, node) .calldata(node, self.default_resolution_context())
.await?; .await?;
code.extend(calldata); code.extend(calldata);
} }
@@ -553,7 +717,6 @@ where
}; };
let Some(address) = receipt.contract_address else { let Some(address) = receipt.contract_address else {
tracing::error!("Contract deployment transaction didn't return an address");
anyhow::bail!("Contract deployment didn't return an address"); anyhow::bail!("Contract deployment didn't return an address");
}; };
tracing::info!( tracing::info!(
@@ -561,18 +724,27 @@ where
instance_address = ?address, instance_address = ?address,
"Deployed contract" "Deployed contract"
); );
self.execution_reporter
.report_contract_deployed_event(contract_instance.clone(), address)?;
self.deployed_contracts self.deployed_contracts.insert(
.insert(contract_instance.clone(), (address, abi.clone())); contract_instance.clone(),
(contract_ident, address, abi.clone()),
);
Ok((address, abi, Some(receipt))) Ok((address, abi, Some(receipt)))
} }
fn default_resolution_context(&self) -> ResolutionContext<'_> {
ResolutionContext::default()
.with_deployed_contracts(&self.deployed_contracts)
.with_variables(&self.variables)
}
} }
pub struct CaseDriver<'a, Leader: Platform, Follower: Platform> { pub struct CaseDriver<'a, Leader: Platform, Follower: Platform> {
metadata: &'a Metadata, metadata: &'a Metadata,
case: &'a Case, case: &'a Case,
case_idx: CaseIdx,
leader_node: &'a Leader::Blockchain, leader_node: &'a Leader::Blockchain,
follower_node: &'a Follower::Blockchain, follower_node: &'a Follower::Blockchain,
leader_state: CaseState<Leader>, leader_state: CaseState<Leader>,
@@ -588,7 +760,6 @@ where
pub fn new( pub fn new(
metadata: &'a Metadata, metadata: &'a Metadata,
case: &'a Case, case: &'a Case,
case_idx: impl Into<CaseIdx>,
leader_node: &'a L::Blockchain, leader_node: &'a L::Blockchain,
follower_node: &'a F::Blockchain, follower_node: &'a F::Blockchain,
leader_state: CaseState<L>, leader_state: CaseState<L>,
@@ -597,7 +768,6 @@ where
Self { Self {
metadata, metadata,
case, case,
case_idx: case_idx.into(),
leader_node, leader_node,
follower_node, follower_node,
leader_state, leader_state,
@@ -605,78 +775,61 @@ where
} }
} }
pub fn trace_diff_mode(label: &str, diff: &DiffMode) { #[instrument(level = "info", name = "Executing Case", skip_all)]
tracing::trace!("{label} - PRE STATE:");
for (addr, state) in &diff.pre {
Self::trace_account_state(" [pre]", addr, state);
}
tracing::trace!("{label} - POST STATE:");
for (addr, state) in &diff.post {
Self::trace_account_state(" [post]", addr, state);
}
}
fn trace_account_state(prefix: &str, addr: &Address, state: &AccountState) {
tracing::trace!("{prefix} 0x{addr:x}");
if let Some(balance) = &state.balance {
tracing::trace!("{prefix} balance: {balance}");
}
if let Some(nonce) = &state.nonce {
tracing::trace!("{prefix} nonce: {nonce}");
}
if let Some(code) = &state.code {
tracing::trace!("{prefix} code: {code}");
}
}
pub async fn execute(&mut self) -> anyhow::Result<usize> { pub async fn execute(&mut self) -> anyhow::Result<usize> {
if !self let mut steps_executed = 0;
.leader_node for (step_idx, step) in self
.matches_target(self.metadata.targets.as_deref()) .case
|| !self .steps_iterator()
.follower_node .enumerate()
.matches_target(self.metadata.targets.as_deref()) .map(|(idx, v)| (StepIdx::new(idx), v))
{ {
tracing::warn!( let (leader_step_output, follower_step_output) = try_join!(
targets = ?self.metadata.targets, self.leader_state
"Either the leader or follower node do not support the targets of the file" .handle_step(self.metadata, &step, self.leader_node)
); .instrument(info_span!(
return Ok(0); "Handling Step",
} %step_idx,
target = "Leader",
)),
self.follower_state
.handle_step(self.metadata, &step, self.follower_node)
.instrument(info_span!(
"Handling Step",
%step_idx,
target = "Follower",
))
)?;
let mut inputs_executed = 0; match (leader_step_output, follower_step_output) {
for (input_idx, input) in self.case.inputs_iterator().enumerate() { (StepOutput::FunctionCall(..), StepOutput::FunctionCall(..)) => {
let tracing_span = tracing::info_span!("Handling input", input_idx); // TODO: We need to actually work out how/if we will compare the diff between
let _guard = tracing_span.enter(); // the leader and the follower. The diffs are almost guaranteed to be different
// from leader and follower and therefore without an actual strategy for this
let (leader_receipt, _, leader_diff) = self // we have something that's guaranteed to fail. Even a simple call to some
.leader_state // contract will produce two non-equal diffs because on the leader the contract
.handle_input(self.metadata, self.case_idx, &input, self.leader_node) // has address X and on the follower it has address Y. On the leader contract X
.await?; // contains address A in the state and on the follower it contains address B. So
let (follower_receipt, _, follower_diff) = self // this isn't exactly a straightforward thing to do and I'm not even sure that
.follower_state // it's possible to do. Once we have an actual strategy for doing the diffs we
.handle_input(self.metadata, self.case_idx, &input, self.follower_node) // will implement it here. Until then, this remains empty.
.await?; }
(StepOutput::BalanceAssertion, StepOutput::BalanceAssertion) => {}
if leader_diff == follower_diff { (StepOutput::StorageEmptyAssertion, StepOutput::StorageEmptyAssertion) => {}
tracing::debug!("State diffs match between leader and follower."); _ => unreachable!("The two step outputs can not be of a different kind"),
} else {
tracing::debug!("State diffs mismatch between leader and follower.");
Self::trace_diff_mode("Leader", &leader_diff);
Self::trace_diff_mode("Follower", &follower_diff);
} }
if leader_receipt.logs() != follower_receipt.logs() { steps_executed += 1;
tracing::debug!("Log/event mismatch between leader and follower.");
tracing::trace!("Leader logs: {:?}", leader_receipt.logs());
tracing::trace!("Follower logs: {:?}", follower_receipt.logs());
}
inputs_executed += 1;
} }
Ok(inputs_executed) Ok(steps_executed)
} }
} }
#[derive(Clone, Debug)]
#[allow(clippy::large_enum_variant)]
pub enum StepOutput {
FunctionCall(TransactionReceipt, GethTrace, DiffMode),
BalanceAssertion,
StorageEmptyAssertion,
}
+791 -355
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -11,10 +11,14 @@ rust-version.workspace = true
[dependencies] [dependencies]
revive-dt-common = { workspace = true } revive-dt-common = { workspace = true }
revive-common = { workspace = true }
alloy = { workspace = true } alloy = { workspace = true }
alloy-primitives = { workspace = true } alloy-primitives = { workspace = true }
alloy-sol-types = { workspace = true } alloy-sol-types = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
futures = { workspace = true }
regex = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
semver = { workspace = true } semver = { workspace = true }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
@@ -22,3 +26,6 @@ serde_json = { workspace = true }
[dev-dependencies] [dev-dependencies]
tokio = { workspace = true } tokio = { workspace = true }
[lints]
workspace = true
+43 -16
View File
@@ -1,31 +1,50 @@
use serde::Deserialize; use serde::{Deserialize, Serialize};
use revive_dt_common::macros::define_wrapper_type; use revive_dt_common::{macros::define_wrapper_type, types::Mode};
use crate::{ use crate::{
input::{Expected, Input}, input::{Expected, Step},
mode::Mode, mode::ParsedMode,
}; };
#[derive(Debug, Default, Deserialize, Clone, Eq, PartialEq)] #[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq)]
pub struct Case { pub struct Case {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>, pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>, pub comment: Option<String>,
pub modes: Option<Vec<Mode>>,
pub inputs: Vec<Input>, #[serde(skip_serializing_if = "Option::is_none")]
pub modes: Option<Vec<ParsedMode>>,
#[serde(rename = "inputs")]
pub steps: Vec<Step>,
#[serde(skip_serializing_if = "Option::is_none")]
pub group: Option<String>, pub group: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expected: Option<Expected>, pub expected: Option<Expected>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ignore: Option<bool>,
} }
impl Case { impl Case {
pub fn inputs_iterator(&self) -> impl Iterator<Item = Input> { #[allow(irrefutable_let_patterns)]
let inputs_len = self.inputs.len(); pub fn steps_iterator(&self) -> impl Iterator<Item = Step> {
self.inputs let steps_len = self.steps.len();
self.steps
.clone() .clone()
.into_iter() .into_iter()
.enumerate() .enumerate()
.map(move |(idx, mut input)| { .map(move |(idx, mut step)| {
if idx + 1 == inputs_len { let Step::FunctionCall(ref mut input) = step else {
return step;
};
if idx + 1 == steps_len {
if input.expected.is_none() { if input.expected.is_none() {
input.expected = self.expected.clone(); input.expected = self.expected.clone();
} }
@@ -35,16 +54,24 @@ impl Case {
// the case? What are we supposed to do with that final expected field on the // the case? What are we supposed to do with that final expected field on the
// case? // case?
input step
} else { } else {
input step
} }
}) })
} }
pub fn solc_modes(&self) -> Vec<Mode> {
match &self.modes {
Some(modes) => ParsedMode::many_to_modes(modes.iter()).collect(),
None => Mode::all().collect(),
}
}
} }
define_wrapper_type!( define_wrapper_type!(
/// A wrapper type for the index of test cases found in metadata file. /// A wrapper type for the index of test cases found in metadata file.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct CaseIdx(usize); #[serde(transparent)]
pub struct CaseIdx(usize) impl Display, FromStr;
); );
+102 -72
View File
@@ -3,97 +3,127 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use revive_dt_common::iterators::FilesWithExtensionIterator;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::{debug, info};
use crate::metadata::MetadataFile; use crate::metadata::{Metadata, MetadataFile};
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, Hash)] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Corpus { #[serde(untagged)]
pub name: String, pub enum Corpus {
pub path: PathBuf, SinglePath { name: String, path: PathBuf },
MultiplePaths { name: String, paths: Vec<PathBuf> },
} }
impl Corpus { impl Corpus {
/// Try to read and parse the corpus definition file at given `path`. pub fn try_from_path(file_path: impl AsRef<Path>) -> anyhow::Result<Self> {
pub fn try_from_path(path: &Path) -> anyhow::Result<Self> { let mut corpus = File::open(file_path.as_ref())
let file = File::open(path)?; .map_err(anyhow::Error::from)
let mut corpus: Corpus = serde_json::from_reader(file)?; .and_then(|file| serde_json::from_reader::<_, Corpus>(file).map_err(Into::into))?;
// Ensure that the path mentioned in the corpus is relative to the corpus file. for path in corpus.paths_iter_mut() {
// Canonicalizing also helps make the path in any errors unambiguous. *path = file_path
corpus.path = path .as_ref()
.parent() .parent()
.ok_or_else(|| { .ok_or_else(|| {
anyhow::anyhow!("Corpus path '{}' does not point to a file", path.display()) anyhow::anyhow!("Corpus path '{}' does not point to a file", path.display())
})? })?
.canonicalize() .canonicalize()
.map_err(|error| { .map_err(|error| {
anyhow::anyhow!( anyhow::anyhow!(
"Failed to canonicalize path to corpus '{}': {error}", "Failed to canonicalize path to corpus '{}': {error}",
path.display() path.display()
) )
})? })?
.join(corpus.path); .join(path.as_path())
}
Ok(corpus) Ok(corpus)
} }
/// Scan the corpus base directory and return all tests found.
pub fn enumerate_tests(&self) -> Vec<MetadataFile> { pub fn enumerate_tests(&self) -> Vec<MetadataFile> {
let mut tests = Vec::new(); let mut tests = self
collect_metadata(&self.path, &mut tests); .paths_iter()
.flat_map(|root_path| {
if !root_path.is_dir() {
Box::new(std::iter::once(root_path.to_path_buf()))
as Box<dyn Iterator<Item = _>>
} else {
Box::new(
FilesWithExtensionIterator::new(root_path)
.with_use_cached_fs(true)
.with_allowed_extension("sol")
.with_allowed_extension("json"),
)
}
.map(move |metadata_file_path| (root_path, metadata_file_path))
})
.filter_map(|(root_path, metadata_file_path)| {
Metadata::try_from_file(&metadata_file_path)
.or_else(|| {
debug!(
discovered_from = %root_path.display(),
metadata_file_path = %metadata_file_path.display(),
"Skipping file since it doesn't contain valid metadata"
);
None
})
.map(|metadata| MetadataFile {
metadata_file_path,
corpus_file_path: root_path.to_path_buf(),
content: metadata,
})
.inspect(|metadata_file| {
debug!(
metadata_file_path = %metadata_file.relative_path().display(),
"Loaded metadata file"
)
})
})
.collect::<Vec<_>>();
tests.sort_by(|a, b| a.metadata_file_path.cmp(&b.metadata_file_path));
tests.dedup_by(|a, b| a.metadata_file_path == b.metadata_file_path);
info!(
len = tests.len(),
corpus_name = self.name(),
"Found tests in Corpus"
);
tests tests
} }
}
/// Recursively walks `path` and parses any JSON or Solidity file into a test pub fn name(&self) -> &str {
/// definition [Metadata]. match self {
/// Corpus::SinglePath { name, .. } | Corpus::MultiplePaths { name, .. } => name.as_str(),
/// Found tests are inserted into `tests`. }
/// }
/// `path` is expected to be a directory.
pub fn collect_metadata(path: &Path, tests: &mut Vec<MetadataFile>) { pub fn paths_iter(&self) -> impl Iterator<Item = &Path> {
if path.is_dir() { match self {
let dir_entry = match std::fs::read_dir(path) { Corpus::SinglePath { path, .. } => {
Ok(dir_entry) => dir_entry, Box::new(std::iter::once(path.as_path())) as Box<dyn Iterator<Item = _>>
Err(error) => {
tracing::error!("failed to read dir '{}': {error}", path.display());
return;
} }
}; Corpus::MultiplePaths { paths, .. } => {
Box::new(paths.iter().map(|path| path.as_path())) as Box<dyn Iterator<Item = _>>
for entry in dir_entry {
let entry = match entry {
Ok(entry) => entry,
Err(error) => {
tracing::error!("error reading dir entry: {error}");
continue;
}
};
let path = entry.path();
if path.is_dir() {
collect_metadata(&path, tests);
continue;
}
if path.is_file() {
if let Some(metadata) = MetadataFile::try_from_file(&path) {
tests.push(metadata)
}
} }
} }
} else { }
let Some(extension) = path.extension() else {
tracing::error!("Failed to get file extension"); pub fn paths_iter_mut(&mut self) -> impl Iterator<Item = &mut PathBuf> {
return; match self {
}; Corpus::SinglePath { path, .. } => {
if extension.eq_ignore_ascii_case("sol") || extension.eq_ignore_ascii_case("json") { Box::new(std::iter::once(path)) as Box<dyn Iterator<Item = _>>
if let Some(metadata) = MetadataFile::try_from_file(path) {
tests.push(metadata)
} }
} else { Corpus::MultiplePaths { paths, .. } => {
tracing::error!(?extension, "Unsupported file extension"); Box::new(paths.iter_mut()) as Box<dyn Iterator<Item = _>>
}
}
}
pub fn path_count(&self) -> usize {
match self {
Corpus::SinglePath { .. } => 1,
Corpus::MultiplePaths { paths, .. } => paths.len(),
} }
} }
} }
+290 -235
View File
@@ -2,39 +2,109 @@ use std::collections::HashMap;
use alloy::{ use alloy::{
eips::BlockNumberOrTag, eips::BlockNumberOrTag,
hex::ToHexExt, json_abi::Function,
json_abi::JsonAbi,
network::TransactionBuilder, network::TransactionBuilder,
primitives::{Address, Bytes, U256}, primitives::{Address, Bytes, U256},
rpc::types::TransactionRequest, rpc::types::TransactionRequest,
}; };
use alloy_primitives::{FixedBytes, utils::parse_units}; use alloy_primitives::{FixedBytes, utils::parse_units};
use anyhow::Context; use anyhow::Context;
use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, stream};
use semver::VersionReq; use semver::VersionReq;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use revive_dt_common::macros::define_wrapper_type; use revive_dt_common::macros::define_wrapper_type;
use tracing::{Instrument, info_span, instrument};
use crate::metadata::ContractInstance;
use crate::traits::ResolverApi; use crate::traits::ResolverApi;
use crate::{metadata::ContractInstance, traits::ResolutionContext};
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)] /// A test step.
///
/// A test step can be anything. It could be an invocation to a function, an assertion, or any other
/// action that needs to be run or executed on the nodes used in the tests.
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[serde(untagged)]
pub enum Step {
/// A function call or an invocation to some function on some smart contract.
FunctionCall(Box<Input>),
/// A step for performing a balance assertion on some account or contract.
BalanceAssertion(Box<BalanceAssertion>),
/// A step for asserting that the storage of some contract or account is empty.
StorageEmptyAssertion(Box<StorageEmptyAssertion>),
}
define_wrapper_type!(
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct StepIdx(usize) impl Display;
);
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
pub struct Input { pub struct Input {
#[serde(default = "Input::default_caller")] #[serde(default = "Input::default_caller")]
pub caller: Address, pub caller: Address,
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>, pub comment: Option<String>,
#[serde(default = "Input::default_instance")] #[serde(default = "Input::default_instance")]
pub instance: ContractInstance, pub instance: ContractInstance,
pub method: Method, pub method: Method,
#[serde(default)] #[serde(default)]
pub calldata: Calldata, pub calldata: Calldata,
#[serde(skip_serializing_if = "Option::is_none")]
pub expected: Option<Expected>, pub expected: Option<Expected>,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<EtherValue>, pub value: Option<EtherValue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub storage: Option<HashMap<String, Calldata>>, pub storage: Option<HashMap<String, Calldata>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub variable_assignments: Option<VariableAssignments>, pub variable_assignments: Option<VariableAssignments>,
} }
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] #[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
pub struct BalanceAssertion {
/// An optional comment on the balance assertion.
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
/// The address that the balance assertion should be done on.
///
/// This is a string which will be resolved into an address when being processed. Therefore,
/// this could be a normal hex address, a variable such as `Test.address`, or perhaps even a
/// full on variable like `$VARIABLE:Uniswap`. It follows the same resolution rules that are
/// followed in the calldata.
pub address: String,
/// The amount of balance to assert that the account or contract has.
pub expected_balance: U256,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
pub struct StorageEmptyAssertion {
/// An optional comment on the storage empty assertion.
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
/// The address that the balance assertion should be done on.
///
/// This is a string which will be resolved into an address when being processed. Therefore,
/// this could be a normal hex address, a variable such as `Test.address`, or perhaps even a
/// full on variable like `$VARIABLE:Uniswap`. It follows the same resolution rules that are
/// followed in the calldata.
pub address: String,
/// A boolean of whether the storage of the address is empty or not.
pub is_storage_empty: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[serde(untagged)] #[serde(untagged)]
pub enum Expected { pub enum Expected {
Calldata(Calldata), Calldata(Calldata),
@@ -42,17 +112,21 @@ pub enum Expected {
ExpectedMany(Vec<ExpectedOutput>), ExpectedMany(Vec<ExpectedOutput>),
} }
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)] #[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
pub struct ExpectedOutput { pub struct ExpectedOutput {
#[serde(skip_serializing_if = "Option::is_none")]
pub compiler_version: Option<VersionReq>, pub compiler_version: Option<VersionReq>,
#[serde(skip_serializing_if = "Option::is_none")]
pub return_data: Option<Calldata>, pub return_data: Option<Calldata>,
#[serde(skip_serializing_if = "Option::is_none")]
pub events: Option<Vec<Event>>, pub events: Option<Vec<Event>>,
#[serde(default)] #[serde(default)]
pub exception: bool, pub exception: bool,
} }
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)] #[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
pub struct Event { pub struct Event {
#[serde(skip_serializing_if = "Option::is_none")]
pub address: Option<String>, pub address: Option<String>,
pub topics: Vec<String>, pub topics: Vec<String>,
pub values: Calldata, pub values: Calldata,
@@ -109,7 +183,7 @@ pub struct Event {
/// [`Single`]: Calldata::Single /// [`Single`]: Calldata::Single
/// [`Compound`]: Calldata::Compound /// [`Compound`]: Calldata::Compound
/// [reverse polish notation]: https://en.wikipedia.org/wiki/Reverse_Polish_notation /// [reverse polish notation]: https://en.wikipedia.org/wiki/Reverse_Polish_notation
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[serde(untagged)] #[serde(untagged)]
pub enum Calldata { pub enum Calldata {
Single(Bytes), Single(Bytes),
@@ -120,7 +194,7 @@ define_wrapper_type! {
/// This represents an item in the [`Calldata::Compound`] variant. /// This represents an item in the [`Calldata::Compound`] variant.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)] #[serde(transparent)]
pub struct CalldataItem(String); pub struct CalldataItem(String) impl Display;
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
@@ -143,7 +217,7 @@ enum Operation {
} }
/// Specify how the contract is called. /// Specify how the contract is called.
#[derive(Debug, Default, Deserialize, Clone, Eq, PartialEq)] #[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq)]
pub enum Method { pub enum Method {
/// Initiate a deploy transaction, calling contracts constructor. /// Initiate a deploy transaction, calling contracts constructor.
/// ///
@@ -165,10 +239,10 @@ pub enum Method {
define_wrapper_type!( define_wrapper_type!(
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct EtherValue(U256); pub struct EtherValue(U256) impl Display;
); );
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)] #[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
pub struct VariableAssignments { pub struct VariableAssignments {
/// A vector of the variable names to assign to the return data. /// A vector of the variable names to assign to the return data.
/// ///
@@ -187,65 +261,46 @@ impl Input {
ContractInstance::new("Test") ContractInstance::new("Test")
} }
fn instance_to_address( pub async fn encoded_input(
&self, &self,
instance: &ContractInstance, resolver: &impl ResolverApi,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>, context: ResolutionContext<'_>,
) -> anyhow::Result<Address> {
deployed_contracts
.get(instance)
.map(|(a, _)| *a)
.ok_or_else(|| anyhow::anyhow!("instance {instance:?} not deployed"))
}
pub async fn encoded_input<'a>(
&'a self,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
variables: impl Into<Option<&'a HashMap<String, U256>>> + Clone,
chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<Bytes> { ) -> anyhow::Result<Bytes> {
match self.method { match self.method {
Method::Deployer | Method::Fallback => { Method::Deployer | Method::Fallback => {
let calldata = self let calldata = self.calldata.calldata(resolver, context).await?;
.calldata
.calldata(deployed_contracts, variables, chain_state_provider)
.await?;
Ok(calldata.into()) Ok(calldata.into())
} }
Method::FunctionName(ref function_name) => { Method::FunctionName(ref function_name) => {
let Some(abi) = deployed_contracts.get(&self.instance).map(|(_, a)| a) else { let Some(abi) = context.deployed_contract_abi(&self.instance) else {
tracing::error!(
contract_name = self.instance.as_ref(),
available_abis = ?deployed_contracts.keys().collect::<Vec<_>>(),
"Attempted to lookup ABI of contract but it wasn't found"
);
anyhow::bail!("ABI for instance '{}' not found", self.instance.as_ref()); anyhow::bail!("ABI for instance '{}' not found", self.instance.as_ref());
}; };
tracing::trace!("ABI found for instance: {}", &self.instance.as_ref());
// We follow the same logic that's implemented in the matter-labs-tester where they resolve // We follow the same logic that's implemented in the matter-labs-tester where they resolve
// the function name into a function selector and they assume that he function doesn't have // the function name into a function selector and they assume that he function doesn't have
// any existing overloads. // any existing overloads.
// Overloads are handled by providing the full function signature in the "function
// name".
// https://github.com/matter-labs/era-compiler-tester/blob/1dfa7d07cba0734ca97e24704f12dd57f6990c2c/compiler_tester/src/test/case/input/mod.rs#L158-L190 // https://github.com/matter-labs/era-compiler-tester/blob/1dfa7d07cba0734ca97e24704f12dd57f6990c2c/compiler_tester/src/test/case/input/mod.rs#L158-L190
let function = abi let selector = if function_name.contains('(') && function_name.contains(')') {
.functions() Function::parse(function_name)
.find(|function| function.signature().starts_with(function_name)) .context(
.ok_or_else(|| { "Failed to parse the provided function name into a function signature",
anyhow::anyhow!( )?
"Function with name {:?} not found in ABI for the instance {:?}", .selector()
function_name, } else {
&self.instance abi.functions()
) .find(|function| function.signature().starts_with(function_name))
})?; .ok_or_else(|| {
anyhow::anyhow!(
tracing::trace!("Functions found for instance: {}", self.instance.as_ref()); "Function with name {:?} not found in ABI for the instance {:?}",
function_name,
tracing::trace!( &self.instance
"Starting encoding ABI's parameters for instance: {}", )
self.instance.as_ref() })?
); .selector()
};
// Allocating a vector that we will be using for the calldata. The vector size will be: // Allocating a vector that we will be using for the calldata. The vector size will be:
// 4 bytes for the function selector. // 4 bytes for the function selector.
@@ -254,14 +309,9 @@ impl Input {
// We're using indices in the following code in order to avoid the need for us to allocate // We're using indices in the following code in order to avoid the need for us to allocate
// a new buffer for each one of the resolved arguments. // a new buffer for each one of the resolved arguments.
let mut calldata = Vec::<u8>::with_capacity(4 + self.calldata.size_requirement()); let mut calldata = Vec::<u8>::with_capacity(4 + self.calldata.size_requirement());
calldata.extend(function.selector().0); calldata.extend(selector.0);
self.calldata self.calldata
.calldata_into_slice( .calldata_into_slice(&mut calldata, resolver, context)
&mut calldata,
deployed_contracts,
variables,
chain_state_provider,
)
.await?; .await?;
Ok(calldata.into()) Ok(calldata.into())
@@ -270,15 +320,12 @@ impl Input {
} }
/// Parse this input into a legacy transaction. /// Parse this input into a legacy transaction.
pub async fn legacy_transaction<'a>( pub async fn legacy_transaction(
&'a self, &self,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>, resolver: &impl ResolverApi,
variables: impl Into<Option<&'a HashMap<String, U256>>> + Clone, context: ResolutionContext<'_>,
chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<TransactionRequest> { ) -> anyhow::Result<TransactionRequest> {
let input_data = self let input_data = self.encoded_input(resolver, context).await?;
.encoded_input(deployed_contracts, variables, chain_state_provider)
.await?;
let transaction_request = TransactionRequest::default().from(self.caller).value( let transaction_request = TransactionRequest::default().from(self.caller).value(
self.value self.value
.map(|value| value.into_inner()) .map(|value| value.into_inner())
@@ -287,7 +334,10 @@ impl Input {
match self.method { match self.method {
Method::Deployer => Ok(transaction_request.with_deploy_code(input_data)), Method::Deployer => Ok(transaction_request.with_deploy_code(input_data)),
_ => Ok(transaction_request _ => Ok(transaction_request
.to(self.instance_to_address(&self.instance, deployed_contracts)?) .to(context
.deployed_contract_address(&self.instance)
.context("Failed to get the contract address")
.copied()?)
.input(input_data.into())), .input(input_data.into())),
} }
} }
@@ -356,49 +406,40 @@ impl Calldata {
} }
} }
pub async fn calldata<'a>( pub async fn calldata(
&'a self, &self,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>, resolver: &impl ResolverApi,
variables: impl Into<Option<&'a HashMap<String, U256>>> + Clone, context: ResolutionContext<'_>,
chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<Vec<u8>> { ) -> anyhow::Result<Vec<u8>> {
let mut buffer = Vec::<u8>::with_capacity(self.size_requirement()); let mut buffer = Vec::<u8>::with_capacity(self.size_requirement());
self.calldata_into_slice( self.calldata_into_slice(&mut buffer, resolver, context)
&mut buffer, .await?;
deployed_contracts,
variables,
chain_state_provider,
)
.await?;
Ok(buffer) Ok(buffer)
} }
pub async fn calldata_into_slice<'a>( pub async fn calldata_into_slice(
&'a self, &self,
buffer: &mut Vec<u8>, buffer: &mut Vec<u8>,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>, resolver: &impl ResolverApi,
variables: impl Into<Option<&'a HashMap<String, U256>>> + Clone, context: ResolutionContext<'_>,
chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
match self { match self {
Calldata::Single(bytes) => { Calldata::Single(bytes) => {
buffer.extend_from_slice(bytes); buffer.extend_from_slice(bytes);
} }
Calldata::Compound(items) => { Calldata::Compound(items) => {
for (arg_idx, arg) in items.iter().enumerate() { let resolved = stream::iter(items.iter().enumerate())
match arg .map(|(arg_idx, arg)| async move {
.resolve(deployed_contracts, variables.clone(), chain_state_provider) arg.resolve(resolver, context)
.await .instrument(info_span!("Resolving argument", %arg, arg_idx))
{ .map_ok(|value| value.to_be_bytes::<32>())
Ok(resolved) => { .await
buffer.extend(resolved.to_be_bytes::<32>()); })
} .buffered(0xFF)
Err(error) => { .try_collect::<Vec<_>>()
tracing::error!(?arg, arg_idx, ?error, "Failed to resolve argument"); .await?;
return Err(error);
} buffer.extend(resolved.into_iter().flatten());
};
}
} }
}; };
Ok(()) Ok(())
@@ -412,59 +453,56 @@ impl Calldata {
} }
/// Checks if this [`Calldata`] is equivalent to the passed calldata bytes. /// Checks if this [`Calldata`] is equivalent to the passed calldata bytes.
pub async fn is_equivalent<'a>( pub async fn is_equivalent(
&'a self, &self,
other: &[u8], other: &[u8],
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>, resolver: &impl ResolverApi,
variables: impl Into<Option<&'a HashMap<String, U256>>> + Clone, context: ResolutionContext<'_>,
chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<bool> { ) -> anyhow::Result<bool> {
match self { match self {
Calldata::Single(calldata) => Ok(calldata == other), Calldata::Single(calldata) => Ok(calldata == other),
Calldata::Compound(items) => { Calldata::Compound(items) => {
// Chunking the "other" calldata into 32 byte chunks since each stream::iter(items.iter().zip(other.chunks(32)))
// one of the items in the compound calldata represents 32 bytes .map(|(this, other)| async move {
for (this, other) in items.iter().zip(other.chunks(32)) { // The matterlabs format supports wildcards and therefore we
// The matterlabs format supports wildcards and therefore we // also need to support them.
// also need to support them. if this.as_ref() == "*" {
if this.as_ref() == "*" { return Ok::<_, anyhow::Error>(true);
continue; }
}
let other = if other.len() < 32 { let other = if other.len() < 32 {
let mut vec = other.to_vec(); let mut vec = other.to_vec();
vec.resize(32, 0); vec.resize(32, 0);
std::borrow::Cow::Owned(vec) std::borrow::Cow::Owned(vec)
} else { } else {
std::borrow::Cow::Borrowed(other) std::borrow::Cow::Borrowed(other)
}; };
let this = this let this = this.resolve(resolver, context).await?;
.resolve(deployed_contracts, variables.clone(), chain_state_provider) let other = U256::from_be_slice(&other);
.await?; Ok(this == other)
let other = U256::from_be_slice(&other); })
if this != other { .buffered(0xFF)
return Ok(false); .all(|v| async move { v.is_ok_and(|v| v) })
} .map(Ok)
} .await
Ok(true)
} }
} }
} }
} }
impl CalldataItem { impl CalldataItem {
async fn resolve<'a>( #[instrument(level = "info", skip_all, err)]
&'a self, async fn resolve(
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>, &self,
variables: impl Into<Option<&'a HashMap<String, U256>>> + Clone, resolver: &impl ResolverApi,
chain_state_provider: &impl ResolverApi, context: ResolutionContext<'_>,
) -> anyhow::Result<U256> { ) -> anyhow::Result<U256> {
let mut stack = Vec::<CalldataToken<U256>>::new(); let mut stack = Vec::<CalldataToken<U256>>::new();
for token in self for token in self
.calldata_tokens() .calldata_tokens()
.map(|token| token.resolve(deployed_contracts, variables.clone(), chain_state_provider)) .map(|token| token.resolve(resolver, context))
{ {
let token = token.await?; let token = token.await?;
let new_token = match token { let new_token = match token {
@@ -505,21 +543,14 @@ impl CalldataItem {
match stack.as_slice() { match stack.as_slice() {
// Empty stack means that we got an empty compound calldata which we resolve to zero. // Empty stack means that we got an empty compound calldata which we resolve to zero.
[] => Ok(U256::ZERO), [] => Ok(U256::ZERO),
[CalldataToken::Item(item)] => { [CalldataToken::Item(item)] => Ok(*item),
tracing::debug!(
original = self.0,
resolved = item.to_be_bytes::<32>().encode_hex(),
"Resolved a Calldata item"
);
Ok(*item)
}
_ => Err(anyhow::anyhow!( _ => Err(anyhow::anyhow!(
"Invalid calldata arithmetic operation - Invalid stack" "Invalid calldata arithmetic operation - Invalid stack"
)), )),
} }
} }
fn calldata_tokens<'a>(&'a self) -> impl Iterator<Item = CalldataToken<&'a str>> + 'a { fn calldata_tokens(&self) -> impl Iterator<Item = CalldataToken<&str>> {
self.0.split(' ').map(|item| match item { self.0.split(' ').map(|item| match item {
"+" => CalldataToken::Operation(Operation::Addition), "+" => CalldataToken::Operation(Operation::Addition),
"-" => CalldataToken::Operation(Operation::Subtraction), "-" => CalldataToken::Operation(Operation::Subtraction),
@@ -543,9 +574,11 @@ impl<T> CalldataToken<T> {
const GAS_LIMIT_VARIABLE: &str = "$GAS_LIMIT"; const GAS_LIMIT_VARIABLE: &str = "$GAS_LIMIT";
const COINBASE_VARIABLE: &str = "$COINBASE"; const COINBASE_VARIABLE: &str = "$COINBASE";
const DIFFICULTY_VARIABLE: &str = "$DIFFICULTY"; const DIFFICULTY_VARIABLE: &str = "$DIFFICULTY";
const BLOCK_BASE_FEE_VARIABLE: &str = "$BASE_FEE";
const BLOCK_HASH_VARIABLE_PREFIX: &str = "$BLOCK_HASH"; const BLOCK_HASH_VARIABLE_PREFIX: &str = "$BLOCK_HASH";
const BLOCK_NUMBER_VARIABLE: &str = "$BLOCK_NUMBER"; const BLOCK_NUMBER_VARIABLE: &str = "$BLOCK_NUMBER";
const BLOCK_TIMESTAMP_VARIABLE: &str = "$BLOCK_TIMESTAMP"; const BLOCK_TIMESTAMP_VARIABLE: &str = "$BLOCK_TIMESTAMP";
const TRANSACTION_GAS_PRICE: &str = "$TRANSACTION_GAS_PRICE";
const VARIABLE_PREFIX: &str = "$VARIABLE:"; const VARIABLE_PREFIX: &str = "$VARIABLE:";
fn into_item(self) -> Option<T> { fn into_item(self) -> Option<T> {
@@ -565,24 +598,21 @@ impl<T: AsRef<str>> CalldataToken<T> {
/// This piece of code is taken from the matter-labs-tester repository which is licensed under /// This piece of code is taken from the matter-labs-tester repository which is licensed under
/// MIT or Apache. The original source code can be found here: /// MIT or Apache. The original source code can be found here:
/// https://github.com/matter-labs/era-compiler-tester/blob/0ed598a27f6eceee7008deab3ff2311075a2ec69/compiler_tester/src/test/case/input/value.rs#L43-L146 /// https://github.com/matter-labs/era-compiler-tester/blob/0ed598a27f6eceee7008deab3ff2311075a2ec69/compiler_tester/src/test/case/input/value.rs#L43-L146
async fn resolve<'a>( async fn resolve(
self, self,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>, resolver: &impl ResolverApi,
variables: impl Into<Option<&'a HashMap<String, U256>>> + Clone, context: ResolutionContext<'_>,
chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<CalldataToken<U256>> { ) -> anyhow::Result<CalldataToken<U256>> {
match self { match self {
Self::Item(item) => { Self::Item(item) => {
let item = item.as_ref(); let item = item.as_ref();
let value = if let Some(instance) = item.strip_suffix(Self::ADDRESS_VARIABLE_SUFFIX) let value = if let Some(instance) = item.strip_suffix(Self::ADDRESS_VARIABLE_SUFFIX)
{ {
Ok(U256::from_be_slice( context
deployed_contracts .deployed_contract_address(&ContractInstance::new(instance))
.get(&ContractInstance::new(instance)) .ok_or_else(|| anyhow::anyhow!("Instance `{}` not found", instance))
.map(|(a, _)| *a) .map(AsRef::as_ref)
.ok_or_else(|| anyhow::anyhow!("Instance `{}` not found", instance))? .map(U256::from_be_slice)
.as_ref(),
))
} else if let Some(value) = item.strip_prefix(Self::NEGATIVE_VALUE_PREFIX) { } else if let Some(value) = item.strip_prefix(Self::NEGATIVE_VALUE_PREFIX) {
let value = U256::from_str_radix(value, 10).map_err(|error| { let value = U256::from_str_radix(value, 10).map_err(|error| {
anyhow::anyhow!("Invalid decimal literal after `-`: {}", error) anyhow::anyhow!("Invalid decimal literal after `-`: {}", error)
@@ -595,27 +625,36 @@ impl<T: AsRef<str>> CalldataToken<T> {
.ok_or_else(|| anyhow::anyhow!("`-0` is invalid literal"))?; .ok_or_else(|| anyhow::anyhow!("`-0` is invalid literal"))?;
Ok(U256::MAX.checked_sub(value).expect("Always valid")) Ok(U256::MAX.checked_sub(value).expect("Always valid"))
} else if let Some(value) = item.strip_prefix(Self::HEX_LITERAL_PREFIX) { } else if let Some(value) = item.strip_prefix(Self::HEX_LITERAL_PREFIX) {
Ok(U256::from_str_radix(value, 16).map_err(|error| { U256::from_str_radix(value, 16)
anyhow::anyhow!("Invalid hexadecimal literal: {}", error) .map_err(|error| anyhow::anyhow!("Invalid hexadecimal literal: {}", error))
})?)
} else if item == Self::CHAIN_VARIABLE { } else if item == Self::CHAIN_VARIABLE {
let chain_id = chain_state_provider.chain_id().await?; resolver.chain_id().await.map(U256::from)
Ok(U256::from(chain_id)) } else if item == Self::TRANSACTION_GAS_PRICE {
context
.transaction_hash()
.context("No transaction hash provided to get the transaction gas price")
.map(|tx_hash| resolver.transaction_gas_price(tx_hash))?
.await
.map(U256::from)
} else if item == Self::GAS_LIMIT_VARIABLE { } else if item == Self::GAS_LIMIT_VARIABLE {
let gas_limit = chain_state_provider resolver
.block_gas_limit(BlockNumberOrTag::Latest) .block_gas_limit(context.resolve_block_number(BlockNumberOrTag::Latest))
.await?; .await
Ok(U256::from(gas_limit)) .map(U256::from)
} else if item == Self::COINBASE_VARIABLE { } else if item == Self::COINBASE_VARIABLE {
let coinbase = chain_state_provider resolver
.block_coinbase(BlockNumberOrTag::Latest) .block_coinbase(context.resolve_block_number(BlockNumberOrTag::Latest))
.await?; .await
Ok(U256::from_be_slice(coinbase.as_ref())) .map(|address| U256::from_be_slice(address.as_ref()))
} else if item == Self::DIFFICULTY_VARIABLE { } else if item == Self::DIFFICULTY_VARIABLE {
let block_difficulty = chain_state_provider resolver
.block_difficulty(BlockNumberOrTag::Latest) .block_difficulty(context.resolve_block_number(BlockNumberOrTag::Latest))
.await?; .await
Ok(block_difficulty) } else if item == Self::BLOCK_BASE_FEE_VARIABLE {
resolver
.block_base_fee(context.resolve_block_number(BlockNumberOrTag::Latest))
.await
.map(U256::from)
} else if item.starts_with(Self::BLOCK_HASH_VARIABLE_PREFIX) { } else if item.starts_with(Self::BLOCK_HASH_VARIABLE_PREFIX) {
let offset: u64 = item let offset: u64 = item
.split(':') .split(':')
@@ -623,35 +662,34 @@ impl<T: AsRef<str>> CalldataToken<T> {
.and_then(|value| value.parse().ok()) .and_then(|value| value.parse().ok())
.unwrap_or_default(); .unwrap_or_default();
let current_block_number = chain_state_provider.last_block_number().await?; let current_block_number = match context.tip_block_number() {
let desired_block_number = current_block_number - offset; Some(block_number) => *block_number,
None => resolver.last_block_number().await?,
};
let desired_block_number = current_block_number.saturating_sub(offset);
let block_hash = chain_state_provider let block_hash = resolver.block_hash(desired_block_number.into()).await?;
.block_hash(desired_block_number.into())
.await?;
Ok(U256::from_be_bytes(block_hash.0)) Ok(U256::from_be_bytes(block_hash.0))
} else if item == Self::BLOCK_NUMBER_VARIABLE { } else if item == Self::BLOCK_NUMBER_VARIABLE {
let current_block_number = chain_state_provider.last_block_number().await?; let current_block_number = match context.tip_block_number() {
Some(block_number) => *block_number,
None => resolver.last_block_number().await?,
};
Ok(U256::from(current_block_number)) Ok(U256::from(current_block_number))
} else if item == Self::BLOCK_TIMESTAMP_VARIABLE { } else if item == Self::BLOCK_TIMESTAMP_VARIABLE {
let timestamp = chain_state_provider resolver
.block_timestamp(BlockNumberOrTag::Latest) .block_timestamp(context.resolve_block_number(BlockNumberOrTag::Latest))
.await?; .await
Ok(U256::from(timestamp)) .map(U256::from)
} else if let Some(variable_name) = item.strip_prefix(Self::VARIABLE_PREFIX) { } else if let Some(variable_name) = item.strip_prefix(Self::VARIABLE_PREFIX) {
let Some(variables) = variables.into() else { context
anyhow::bail!( .variable(variable_name)
"Variable resolution required but no variables were passed in" .context("Variable lookup failed")
); .copied()
};
let Some(variable) = variables.get(variable_name) else {
anyhow::bail!("No variable found with the name {}", variable_name)
};
Ok(*variable)
} else { } else {
Ok(U256::from_str_radix(item, 10) U256::from_str_radix(item, 10)
.map_err(|error| anyhow::anyhow!("Invalid decimal literal: {}", error))?) .map_err(|error| anyhow::anyhow!("Invalid decimal literal: {}", error))
}; };
value.map(CalldataToken::Item) value.map(CalldataToken::Item)
} }
@@ -689,51 +727,52 @@ impl<'de> Deserialize<'de> for EtherValue {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use alloy::{eips::BlockNumberOrTag, json_abi::JsonAbi};
use alloy::json_abi::JsonAbi; use alloy_primitives::{BlockHash, BlockNumber, BlockTimestamp, ChainId, TxHash, address};
use alloy_primitives::address;
use alloy_sol_types::SolValue; use alloy_sol_types::SolValue;
use std::collections::HashMap; use std::collections::HashMap;
use super::*;
use crate::metadata::ContractIdent;
struct MockResolver; struct MockResolver;
impl ResolverApi for MockResolver { impl ResolverApi for MockResolver {
async fn chain_id(&self) -> anyhow::Result<alloy_primitives::ChainId> { async fn chain_id(&self) -> anyhow::Result<ChainId> {
Ok(0x123) Ok(0x123)
} }
async fn block_gas_limit(&self, _: alloy::eips::BlockNumberOrTag) -> anyhow::Result<u128> { async fn block_gas_limit(&self, _: BlockNumberOrTag) -> anyhow::Result<u128> {
Ok(0x1234) Ok(0x1234)
} }
async fn block_coinbase( async fn block_coinbase(&self, _: BlockNumberOrTag) -> anyhow::Result<Address> {
&self,
_: alloy::eips::BlockNumberOrTag,
) -> anyhow::Result<Address> {
Ok(Address::ZERO) Ok(Address::ZERO)
} }
async fn block_difficulty(&self, _: alloy::eips::BlockNumberOrTag) -> anyhow::Result<U256> { async fn block_difficulty(&self, _: BlockNumberOrTag) -> anyhow::Result<U256> {
Ok(U256::from(0x12345u128)) Ok(U256::from(0x12345u128))
} }
async fn block_hash( async fn block_base_fee(&self, _: BlockNumberOrTag) -> anyhow::Result<u64> {
&self, Ok(0x100)
_: alloy::eips::BlockNumberOrTag, }
) -> anyhow::Result<alloy_primitives::BlockHash> {
async fn block_hash(&self, _: BlockNumberOrTag) -> anyhow::Result<BlockHash> {
Ok([0xEE; 32].into()) Ok([0xEE; 32].into())
} }
async fn block_timestamp( async fn block_timestamp(&self, _: BlockNumberOrTag) -> anyhow::Result<BlockTimestamp> {
&self,
_: alloy::eips::BlockNumberOrTag,
) -> anyhow::Result<alloy_primitives::BlockTimestamp> {
Ok(0x123456) Ok(0x123456)
} }
async fn last_block_number(&self) -> anyhow::Result<alloy_primitives::BlockNumber> { async fn last_block_number(&self) -> anyhow::Result<BlockNumber> {
Ok(0x1234567) Ok(0x1234567)
} }
async fn transaction_gas_price(&self, _: &TxHash) -> anyhow::Result<u128> {
Ok(0x200)
}
} }
#[tokio::test] #[tokio::test]
@@ -769,13 +808,12 @@ mod tests {
let mut contracts = HashMap::new(); let mut contracts = HashMap::new();
contracts.insert( contracts.insert(
ContractInstance::new("Contract"), ContractInstance::new("Contract"),
(Address::ZERO, parsed_abi), (ContractIdent::new("Contract"), Address::ZERO, parsed_abi),
); );
let encoded = input let resolver = MockResolver;
.encoded_input(&contracts, None, &MockResolver) let context = ResolutionContext::default().with_deployed_contracts(&contracts);
.await let encoded = input.encoded_input(&resolver, context).await.unwrap();
.unwrap();
assert!(encoded.0.starts_with(&selector)); assert!(encoded.0.starts_with(&selector));
type T = (u64,); type T = (u64,);
@@ -814,13 +852,12 @@ mod tests {
let mut contracts = HashMap::new(); let mut contracts = HashMap::new();
contracts.insert( contracts.insert(
ContractInstance::new("Contract"), ContractInstance::new("Contract"),
(Address::ZERO, parsed_abi), (ContractIdent::new("Contract"), Address::ZERO, parsed_abi),
); );
let encoded = input let resolver = MockResolver;
.encoded_input(&contracts, None, &MockResolver) let context = ResolutionContext::default().with_deployed_contracts(&contracts);
.await let encoded = input.encoded_input(&resolver, context).await.unwrap();
.unwrap();
assert!(encoded.0.starts_with(&selector)); assert!(encoded.0.starts_with(&selector));
type T = (alloy_primitives::Address,); type T = (alloy_primitives::Address,);
@@ -862,13 +899,12 @@ mod tests {
let mut contracts = HashMap::new(); let mut contracts = HashMap::new();
contracts.insert( contracts.insert(
ContractInstance::new("Contract"), ContractInstance::new("Contract"),
(Address::ZERO, parsed_abi), (ContractIdent::new("Contract"), Address::ZERO, parsed_abi),
); );
let encoded = input let resolver = MockResolver;
.encoded_input(&contracts, None, &MockResolver) let context = ResolutionContext::default().with_deployed_contracts(&contracts);
.await let encoded = input.encoded_input(&resolver, context).await.unwrap();
.unwrap();
assert!(encoded.0.starts_with(&selector)); assert!(encoded.0.starts_with(&selector));
type T = (alloy_primitives::Address,); type T = (alloy_primitives::Address,);
@@ -881,12 +917,11 @@ mod tests {
async fn resolve_calldata_item( async fn resolve_calldata_item(
input: &str, input: &str,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>, deployed_contracts: &HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>,
chain_state_provider: &impl ResolverApi, resolver: &impl ResolverApi,
) -> anyhow::Result<U256> { ) -> anyhow::Result<U256> {
CalldataItem::new(input) let context = ResolutionContext::default().with_deployed_contracts(deployed_contracts);
.resolve(deployed_contracts, None, chain_state_provider) CalldataItem::new(input).resolve(resolver, context).await
.await
} }
#[tokio::test] #[tokio::test]
@@ -964,6 +999,26 @@ mod tests {
) )
} }
#[tokio::test]
async fn resolver_can_resolve_block_base_fee_variable() {
// Arrange
let input = "$BASE_FEE";
// Act
let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver).await;
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(
resolved,
MockResolver
.block_base_fee(Default::default())
.await
.map(U256::from)
.unwrap()
)
}
#[tokio::test] #[tokio::test]
async fn resolver_can_resolve_block_hash_variable() { async fn resolver_can_resolve_block_hash_variable() {
// Arrange // Arrange
+226 -64
View File
@@ -1,7 +1,8 @@
use std::{ use std::{
cmp::Ordering,
collections::BTreeMap, collections::BTreeMap,
fmt::Display, fmt::Display,
fs::{File, read_to_string}, fs::File,
ops::Deref, ops::Deref,
path::{Path, PathBuf}, path::{Path, PathBuf},
str::FromStr, str::FromStr,
@@ -9,12 +10,14 @@ use std::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use revive_dt_common::{iterators::FilesWithExtensionIterator, macros::define_wrapper_type}; use revive_common::EVMVersion;
use revive_dt_common::{
use crate::{ cached_fs::read_to_string, iterators::FilesWithExtensionIterator, macros::define_wrapper_type,
case::Case, types::Mode,
mode::{Mode, SolcMode},
}; };
use tracing::error;
use crate::{case::Case, mode::ParsedMode};
pub const METADATA_FILE_EXTENSION: &str = "json"; pub const METADATA_FILE_EXTENSION: &str = "json";
pub const SOLIDITY_CASE_FILE_EXTENSION: &str = "sol"; pub const SOLIDITY_CASE_FILE_EXTENSION: &str = "sol";
@@ -22,16 +25,26 @@ pub const SOLIDITY_CASE_COMMENT_MARKER: &str = "//!";
#[derive(Debug, Default, Deserialize, Clone, Eq, PartialEq)] #[derive(Debug, Default, Deserialize, Clone, Eq, PartialEq)]
pub struct MetadataFile { pub struct MetadataFile {
pub path: PathBuf, /// The path of the metadata file. This will either be a JSON or solidity file.
pub metadata_file_path: PathBuf,
/// This is the path contained within the corpus file. This could either be the path of some dir
/// or could be the actual metadata file path.
pub corpus_file_path: PathBuf,
/// The metadata contained within the file.
pub content: Metadata, pub content: Metadata,
} }
impl MetadataFile { impl MetadataFile {
pub fn try_from_file(path: &Path) -> Option<Self> { pub fn relative_path(&self) -> &Path {
Metadata::try_from_file(path).map(|metadata| Self { if self.corpus_file_path.is_file() {
path: path.to_owned(), &self.corpus_file_path
content: metadata, } else {
}) self.metadata_file_path
.strip_prefix(&self.corpus_file_path)
.unwrap()
}
} }
} }
@@ -43,34 +56,51 @@ impl Deref for MetadataFile {
} }
} }
#[derive(Debug, Default, Deserialize, Clone, Eq, PartialEq)] #[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq)]
pub struct Metadata { pub struct Metadata {
pub targets: Option<Vec<String>>, /// A comment on the test case that's added for human-readability.
pub cases: Vec<Case>, #[serde(skip_serializing_if = "Option::is_none")]
pub contracts: Option<BTreeMap<ContractInstance, ContractPathAndIdent>>, pub comment: Option<String>,
// TODO: Convert into wrapper types for clarity.
pub libraries: Option<BTreeMap<PathBuf, BTreeMap<ContractIdent, ContractInstance>>>, #[serde(skip_serializing_if = "Option::is_none")]
pub ignore: Option<bool>, pub ignore: Option<bool>,
pub modes: Option<Vec<Mode>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub targets: Option<Vec<String>>,
pub cases: Vec<Case>,
#[serde(skip_serializing_if = "Option::is_none")]
pub contracts: Option<BTreeMap<ContractInstance, ContractPathAndIdent>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub libraries: Option<BTreeMap<PathBuf, BTreeMap<ContractIdent, ContractInstance>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub modes: Option<Vec<ParsedMode>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_path: Option<PathBuf>, pub file_path: Option<PathBuf>,
/// This field specifies an EVM version requirement that the test case has where the test might
/// be run of the evm version of the nodes match the evm version specified here.
#[serde(skip_serializing_if = "Option::is_none")]
pub required_evm_version: Option<EvmVersionRequirement>,
/// A set of compilation directives that will be passed to the compiler whenever the contracts for
/// the test are being compiled. Note that this differs from the [`Mode`]s in that a [`Mode`] is
/// just a filter for when a test can run whereas this is an instruction to the compiler.
#[serde(skip_serializing_if = "Option::is_none")]
pub compiler_directives: Option<CompilationDirectives>,
} }
impl Metadata { impl Metadata {
/// Returns the solc modes of this metadata, inserting a default mode if not present. /// Returns the modes that we should test from this metadata.
pub fn solc_modes(&self) -> Vec<SolcMode> { pub fn solc_modes(&self) -> Vec<Mode> {
self.modes match &self.modes {
.to_owned() Some(modes) => ParsedMode::many_to_modes(modes.iter()).collect(),
.unwrap_or_else(|| vec![Mode::Solidity(Default::default())]) None => Mode::all().collect(),
.iter() }
.filter_map(|mode| match mode {
Mode::Solidity(solc_mode) => Some(solc_mode),
Mode::Unknown(mode) => {
tracing::debug!("compiler: ignoring unknown mode '{mode}'");
None
}
})
.cloned()
.collect()
} }
/// Returns the base directory of this metadata. /// Returns the base directory of this metadata.
@@ -126,10 +156,7 @@ impl Metadata {
pub fn try_from_file(path: &Path) -> Option<Self> { pub fn try_from_file(path: &Path) -> Option<Self> {
assert!(path.is_file(), "not a file: {}", path.display()); assert!(path.is_file(), "not a file: {}", path.display());
let Some(file_extension) = path.extension() else { let file_extension = path.extension()?;
tracing::debug!("skipping corpus file: {}", path.display());
return None;
};
if file_extension == METADATA_FILE_EXTENSION { if file_extension == METADATA_FILE_EXTENSION {
return Self::try_from_json(path); return Self::try_from_json(path);
@@ -139,18 +166,12 @@ impl Metadata {
return Self::try_from_solidity(path); return Self::try_from_solidity(path);
} }
tracing::debug!("ignoring invalid corpus file: {}", path.display());
None None
} }
fn try_from_json(path: &Path) -> Option<Self> { fn try_from_json(path: &Path) -> Option<Self> {
let file = File::open(path) let file = File::open(path)
.inspect_err(|error| { .inspect_err(|err| error!(path = %path.display(), %err, "Failed to open file"))
tracing::error!(
"opening JSON test metadata file '{}' error: {error}",
path.display()
);
})
.ok()?; .ok()?;
match serde_json::from_reader::<_, Metadata>(file) { match serde_json::from_reader::<_, Metadata>(file) {
@@ -158,11 +179,8 @@ impl Metadata {
metadata.file_path = Some(path.to_path_buf()); metadata.file_path = Some(path.to_path_buf());
Some(metadata) Some(metadata)
} }
Err(error) => { Err(err) => {
tracing::error!( error!(path = %path.display(), %err, "Deserialization of metadata failed");
"parsing JSON test metadata file '{}' error: {error}",
path.display()
);
None None
} }
} }
@@ -170,12 +188,7 @@ impl Metadata {
fn try_from_solidity(path: &Path) -> Option<Self> { fn try_from_solidity(path: &Path) -> Option<Self> {
let spec = read_to_string(path) let spec = read_to_string(path)
.inspect_err(|error| { .inspect_err(|err| error!(path = %path.display(), %err, "Failed to read file content"))
tracing::error!(
"opening JSON test metadata file '{}' error: {error}",
path.display()
);
})
.ok()? .ok()?
.lines() .lines()
.filter_map(|line| line.strip_prefix(SOLIDITY_CASE_COMMENT_MARKER)) .filter_map(|line| line.strip_prefix(SOLIDITY_CASE_COMMENT_MARKER))
@@ -203,11 +216,8 @@ impl Metadata {
); );
Some(metadata) Some(metadata)
} }
Err(error) => { Err(err) => {
tracing::error!( error!(path = %path.display(), %err, "Failed to deserialize metadata");
"parsing Solidity test metadata file '{}' error: '{error}' from data: {spec}",
path.display()
);
None None
} }
} }
@@ -231,7 +241,9 @@ impl Metadata {
Ok(Box::new(std::iter::once(metadata_file_path.clone()))) Ok(Box::new(std::iter::once(metadata_file_path.clone())))
} else { } else {
Ok(Box::new( Ok(Box::new(
FilesWithExtensionIterator::new(self.directory()?).with_allowed_extension("sol"), FilesWithExtensionIterator::new(self.directory()?)
.with_allowed_extension("sol")
.with_use_cached_fs(true),
)) ))
} }
} }
@@ -245,7 +257,7 @@ define_wrapper_type!(
Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)] )]
#[serde(transparent)] #[serde(transparent)]
pub struct ContractInstance(String); pub struct ContractInstance(String) impl Display;
); );
define_wrapper_type!( define_wrapper_type!(
@@ -256,7 +268,7 @@ define_wrapper_type!(
Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)] )]
#[serde(transparent)] #[serde(transparent)]
pub struct ContractIdent(String); pub struct ContractIdent(String) impl Display;
); );
/// Represents an identifier used for contracts. /// Represents an identifier used for contracts.
@@ -343,6 +355,156 @@ impl From<ContractPathAndIdent> for String {
} }
} }
/// An EVM version requirement that the test case has. This gets serialized and
/// deserialized from and into [`String`].
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct EvmVersionRequirement {
ordering: Ordering,
or_equal: bool,
evm_version: EVMVersion,
}
impl EvmVersionRequirement {
pub fn new_greater_than_or_equals(version: EVMVersion) -> Self {
Self {
ordering: Ordering::Greater,
or_equal: true,
evm_version: version,
}
}
pub fn new_greater_than(version: EVMVersion) -> Self {
Self {
ordering: Ordering::Greater,
or_equal: false,
evm_version: version,
}
}
pub fn new_equals(version: EVMVersion) -> Self {
Self {
ordering: Ordering::Equal,
or_equal: false,
evm_version: version,
}
}
pub fn new_less_than(version: EVMVersion) -> Self {
Self {
ordering: Ordering::Less,
or_equal: false,
evm_version: version,
}
}
pub fn new_less_than_or_equals(version: EVMVersion) -> Self {
Self {
ordering: Ordering::Less,
or_equal: true,
evm_version: version,
}
}
pub fn matches(&self, other: &EVMVersion) -> bool {
let ordering = other.cmp(&self.evm_version);
ordering == self.ordering || (self.or_equal && matches!(ordering, Ordering::Equal))
}
}
impl Display for EvmVersionRequirement {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
ordering,
or_equal,
evm_version,
} = self;
match ordering {
Ordering::Less => write!(f, "<")?,
Ordering::Equal => write!(f, "=")?,
Ordering::Greater => write!(f, ">")?,
}
if *or_equal && !matches!(ordering, Ordering::Equal) {
write!(f, "=")?;
}
write!(f, "{evm_version}")
}
}
impl FromStr for EvmVersionRequirement {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.as_bytes() {
[b'>', b'=', remaining @ ..] => Ok(Self {
ordering: Ordering::Greater,
or_equal: true,
evm_version: str::from_utf8(remaining)?.try_into()?,
}),
[b'>', remaining @ ..] => Ok(Self {
ordering: Ordering::Greater,
or_equal: false,
evm_version: str::from_utf8(remaining)?.try_into()?,
}),
[b'<', b'=', remaining @ ..] => Ok(Self {
ordering: Ordering::Less,
or_equal: true,
evm_version: str::from_utf8(remaining)?.try_into()?,
}),
[b'<', remaining @ ..] => Ok(Self {
ordering: Ordering::Less,
or_equal: false,
evm_version: str::from_utf8(remaining)?.try_into()?,
}),
[b'=', remaining @ ..] => Ok(Self {
ordering: Ordering::Equal,
or_equal: false,
evm_version: str::from_utf8(remaining)?.try_into()?,
}),
_ => anyhow::bail!("Invalid EVM version requirement {s}"),
}
}
}
impl TryFrom<String> for EvmVersionRequirement {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
value.parse()
}
}
impl From<EvmVersionRequirement> for String {
fn from(value: EvmVersionRequirement) -> Self {
value.to_string()
}
}
/// A set of compilation directives that will be passed to the compiler whenever the contracts for
/// the test are being compiled. Note that this differs from the [`Mode`]s in that a [`Mode`] is
/// just a filter for when a test can run whereas this is an instruction to the compiler.
/// Defines how the compiler should handle revert strings.
#[derive(
Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
)]
pub struct CompilationDirectives {
/// Defines how the revert strings should be handled.
pub revert_string_handling: Option<RevertString>,
}
/// Defines how the compiler should handle revert strings.
#[derive(
Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
)]
#[serde(rename_all = "camelCase")]
pub enum RevertString {
#[default]
Default,
Debug,
Strip,
VerboseDebug,
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
+241 -85
View File
@@ -1,106 +1,262 @@
use revive_dt_common::types::VersionOrRequirement; use regex::Regex;
use semver::Version; use revive_dt_common::types::{Mode, ModeOptimizerSetting, ModePipeline};
use serde::de::Deserializer;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fmt::Display;
use std::str::FromStr;
use std::sync::LazyLock;
/// Specifies the compilation mode of the test artifact. /// This represents a mode that has been parsed from test metadata.
#[derive(Hash, Debug, Clone, Eq, PartialEq)] ///
pub enum Mode { /// Mode strings can take the following form (in pseudo-regex):
Solidity(SolcMode), ///
Unknown(String), /// ```text
/// [YEILV][+-]? (M[0123sz])? <semver>?
/// ```
///
/// We can parse valid mode strings into [`ParsedMode`] using [`ParsedMode::from_str`].
#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
#[serde(try_from = "String", into = "String")]
pub struct ParsedMode {
pub pipeline: Option<ModePipeline>,
pub optimize_flag: Option<bool>,
pub optimize_setting: Option<ModeOptimizerSetting>,
pub version: Option<semver::VersionReq>,
} }
/// Specify Solidity specific compiler options. impl FromStr for ParsedMode {
#[derive(Hash, Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)] type Err = anyhow::Error;
pub struct SolcMode { fn from_str(s: &str) -> Result<Self, Self::Err> {
pub solc_version: Option<semver::VersionReq>, static REGEX: LazyLock<Regex> = LazyLock::new(|| {
solc_optimize: Option<bool>, Regex::new(r"(?x)
pub llvm_optimizer_settings: Vec<String>, ^
} (?:(?P<pipeline>[YEILV])(?P<optimize_flag>[+-])?)? # Pipeline to use eg Y, E+, E-
\s*
(?P<optimize_setting>M[a-zA-Z0-9])? # Optimize setting eg M0, Ms, Mz
\s*
(?P<version>[>=<]*\d+(?:\.\d+)*)? # Optional semver version eg >=0.8.0, 0.7, <0.8
$
").unwrap()
});
impl SolcMode { let Some(caps) = REGEX.captures(s) else {
/// Try to parse a mode string into a solc mode. anyhow::bail!("Cannot parse mode '{s}' from string");
/// Returns `None` if the string wasn't a solc YUL mode string.
///
/// The mode string is expected to start with the `Y` ID (YUL ID),
/// optionally followed by `+` or `-` for the solc optimizer settings.
///
/// Options can be separated by a whitespace contain the following
/// - A solc `SemVer version requirement` string
/// - One or more `-OX` where X is a supposed to be an LLVM opt mode
pub fn parse_from_mode_string(mode_string: &str) -> Option<Self> {
let mut result = Self::default();
let mut parts = mode_string.trim().split(" ");
match parts.next()? {
"Y" => {}
"Y+" => result.solc_optimize = Some(true),
"Y-" => result.solc_optimize = Some(false),
_ => return None,
}
for part in parts {
if let Ok(solc_version) = semver::VersionReq::parse(part) {
result.solc_version = Some(solc_version);
continue;
}
if let Some(level) = part.strip_prefix("-O") {
result.llvm_optimizer_settings.push(level.to_string());
continue;
}
panic!("the YUL mode string {mode_string} failed to parse, invalid part: {part}")
}
Some(result)
}
/// Returns whether to enable the solc optimizer.
pub fn solc_optimize(&self) -> bool {
self.solc_optimize.unwrap_or(true)
}
/// Calculate the latest matching solc patch version. Returns:
/// - `latest_supported` if no version request was specified.
/// - A matching version with the same minor version as `latest_supported`, if any.
/// - `None` if no minor version of the `latest_supported` version matches.
pub fn last_patch_version(&self, latest_supported: &Version) -> Option<Version> {
let Some(version_req) = self.solc_version.as_ref() else {
return Some(latest_supported.to_owned());
}; };
// lgtm let pipeline = match caps.name("pipeline") {
for patch in (0..latest_supported.patch + 1).rev() { Some(m) => Some(ModePipeline::from_str(m.as_str())?),
let version = Version::new(0, latest_supported.minor, patch); None => None,
if version_req.matches(&version) { };
return Some(version);
let optimize_flag = caps.name("optimize_flag").map(|m| m.as_str() == "+");
let optimize_setting = match caps.name("optimize_setting") {
Some(m) => Some(ModeOptimizerSetting::from_str(m.as_str())?),
None => None,
};
let version = match caps.name("version") {
Some(m) => Some(semver::VersionReq::parse(m.as_str()).map_err(|e| {
anyhow::anyhow!("Cannot parse the version requirement '{}': {e}", m.as_str())
})?),
None => None,
};
Ok(ParsedMode {
pipeline,
optimize_flag,
optimize_setting,
version,
})
}
}
impl Display for ParsedMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut has_written = false;
if let Some(pipeline) = self.pipeline {
pipeline.fmt(f)?;
if let Some(optimize_flag) = self.optimize_flag {
f.write_str(if optimize_flag { "+" } else { "-" })?;
} }
has_written = true;
} }
None if let Some(optimize_setting) = self.optimize_setting {
if has_written {
f.write_str(" ")?;
}
optimize_setting.fmt(f)?;
has_written = true;
}
if let Some(version) = &self.version {
if has_written {
f.write_str(" ")?;
}
version.fmt(f)?;
}
Ok(())
}
}
impl From<ParsedMode> for String {
fn from(parsed_mode: ParsedMode) -> Self {
parsed_mode.to_string()
}
}
impl TryFrom<String> for ParsedMode {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
ParsedMode::from_str(&value)
}
}
impl ParsedMode {
/// This takes a [`ParsedMode`] and expands it into a list of [`Mode`]s that we should try.
pub fn to_modes(&self) -> impl Iterator<Item = Mode> {
let pipeline_iter = self.pipeline.as_ref().map_or_else(
|| EitherIter::A(ModePipeline::test_cases()),
|p| EitherIter::B(std::iter::once(*p)),
);
let optimize_flag_setting = self.optimize_flag.map(|flag| {
if flag {
ModeOptimizerSetting::M3
} else {
ModeOptimizerSetting::M0
}
});
let optimize_flag_iter = match optimize_flag_setting {
Some(setting) => EitherIter::A(std::iter::once(setting)),
None => EitherIter::B(ModeOptimizerSetting::test_cases()),
};
let optimize_settings_iter = self.optimize_setting.as_ref().map_or_else(
|| EitherIter::A(optimize_flag_iter),
|s| EitherIter::B(std::iter::once(*s)),
);
pipeline_iter.flat_map(move |pipeline| {
optimize_settings_iter
.clone()
.map(move |optimize_setting| Mode {
pipeline,
optimize_setting,
version: self.version.clone(),
})
})
} }
/// Resolves the [`SolcMode`]'s solidity version requirement into a [`VersionOrRequirement`] if /// Return a set of [`Mode`]s that correspond to the given [`ParsedMode`]s.
/// the requirement is present on the object. Otherwise, the passed default version is used. /// This avoids any duplicate entries.
pub fn compiler_version_to_use(&self, default: Version) -> VersionOrRequirement { pub fn many_to_modes<'a>(
match self.solc_version { parsed: impl Iterator<Item = &'a ParsedMode>,
Some(ref requirement) => requirement.clone().into(), ) -> impl Iterator<Item = Mode> {
None => default.into(), let modes: HashSet<_> = parsed.flat_map(|p| p.to_modes()).collect();
modes.into_iter()
}
}
/// An iterator that could be either of two iterators.
#[derive(Clone, Debug)]
enum EitherIter<A, B> {
A(A),
B(B),
}
impl<A, B> Iterator for EitherIter<A, B>
where
A: Iterator,
B: Iterator<Item = A::Item>,
{
type Item = A::Item;
fn next(&mut self) -> Option<Self::Item> {
match self {
EitherIter::A(iter) => iter.next(),
EitherIter::B(iter) => iter.next(),
} }
} }
} }
impl<'de> Deserialize<'de> for Mode { #[cfg(test)]
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> mod tests {
where use super::*;
D: Deserializer<'de>,
{
let mode_string = String::deserialize(deserializer)?;
if let Some(solc_mode) = SolcMode::parse_from_mode_string(&mode_string) { #[test]
return Ok(Self::Solidity(solc_mode)); fn test_parsed_mode_from_str() {
let strings = vec![
("Mz", "Mz"),
("Y", "Y"),
("Y+", "Y+"),
("Y-", "Y-"),
("E", "E"),
("E+", "E+"),
("E-", "E-"),
("Y M0", "Y M0"),
("Y M1", "Y M1"),
("Y M2", "Y M2"),
("Y M3", "Y M3"),
("Y Ms", "Y Ms"),
("Y Mz", "Y Mz"),
("E M0", "E M0"),
("E M1", "E M1"),
("E M2", "E M2"),
("E M3", "E M3"),
("E Ms", "E Ms"),
("E Mz", "E Mz"),
// When stringifying semver again, 0.8.0 becomes ^0.8.0 (same meaning)
("Y 0.8.0", "Y ^0.8.0"),
("E+ 0.8.0", "E+ ^0.8.0"),
("Y M3 >=0.8.0", "Y M3 >=0.8.0"),
("E Mz <0.7.0", "E Mz <0.7.0"),
// We can parse +- _and_ M1/M2 but the latter takes priority.
("Y+ M1 0.8.0", "Y+ M1 ^0.8.0"),
("E- M2 0.7.0", "E- M2 ^0.7.0"),
// We don't see this in the wild but it is parsed.
("<=0.8", "<=0.8"),
];
for (actual, expected) in strings {
let parsed = ParsedMode::from_str(actual)
.unwrap_or_else(|_| panic!("Failed to parse mode string '{actual}'"));
assert_eq!(
expected,
parsed.to_string(),
"Mode string '{actual}' did not parse to '{expected}': got '{parsed}'"
);
} }
}
Ok(Self::Unknown(mode_string)) #[test]
fn test_parsed_mode_to_test_modes() {
let strings = vec![
("Mz", vec!["Y Mz", "E Mz"]),
("Y", vec!["Y M0", "Y M3"]),
("E", vec!["E M0", "E M3"]),
("Y+", vec!["Y M3"]),
("Y-", vec!["Y M0"]),
("Y <=0.8", vec!["Y M0 <=0.8", "Y M3 <=0.8"]),
(
"<=0.8",
vec!["Y M0 <=0.8", "Y M3 <=0.8", "E M0 <=0.8", "E M3 <=0.8"],
),
];
for (actual, expected) in strings {
let parsed = ParsedMode::from_str(actual)
.unwrap_or_else(|_| panic!("Failed to parse mode string '{actual}'"));
let expected_set: HashSet<_> = expected.into_iter().map(|s| s.to_owned()).collect();
let actual_set: HashSet<_> = parsed.to_modes().map(|m| m.to_string()).collect();
assert_eq!(
expected_set, actual_set,
"Mode string '{actual}' did not expand to '{expected_set:?}': got '{actual_set:?}'"
);
}
} }
} }
+124
View File
@@ -1,13 +1,22 @@
use std::collections::HashMap;
use alloy::eips::BlockNumberOrTag; use alloy::eips::BlockNumberOrTag;
use alloy::json_abi::JsonAbi;
use alloy::primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, ChainId, U256}; use alloy::primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, ChainId, U256};
use alloy_primitives::TxHash;
use anyhow::Result; use anyhow::Result;
use crate::metadata::{ContractIdent, ContractInstance};
/// A trait of the interface are required to implement to be used by the resolution logic that this /// A trait of the interface are required to implement to be used by the resolution logic that this
/// crate implements to go from string calldata and into the bytes calldata. /// crate implements to go from string calldata and into the bytes calldata.
pub trait ResolverApi { pub trait ResolverApi {
/// Returns the ID of the chain that the node is on. /// Returns the ID of the chain that the node is on.
fn chain_id(&self) -> impl Future<Output = Result<ChainId>>; fn chain_id(&self) -> impl Future<Output = Result<ChainId>>;
/// Returns the gas price for the specified transaction.
fn transaction_gas_price(&self, tx_hash: &TxHash) -> impl Future<Output = Result<u128>>;
// TODO: This is currently a u128 due to Kitchensink needing more than 64 bits for its gas limit // TODO: This is currently a u128 due to Kitchensink needing more than 64 bits for its gas limit
// when we implement the changes to the gas we need to adjust this to be a u64. // when we implement the changes to the gas we need to adjust this to be a u64.
/// Returns the gas limit of the specified block. /// Returns the gas limit of the specified block.
@@ -19,6 +28,9 @@ pub trait ResolverApi {
/// Returns the difficulty of the specified block. /// Returns the difficulty of the specified block.
fn block_difficulty(&self, number: BlockNumberOrTag) -> impl Future<Output = Result<U256>>; fn block_difficulty(&self, number: BlockNumberOrTag) -> impl Future<Output = Result<U256>>;
/// Returns the base fee of the specified block.
fn block_base_fee(&self, number: BlockNumberOrTag) -> impl Future<Output = Result<u64>>;
/// Returns the hash of the specified block. /// Returns the hash of the specified block.
fn block_hash(&self, number: BlockNumberOrTag) -> impl Future<Output = Result<BlockHash>>; fn block_hash(&self, number: BlockNumberOrTag) -> impl Future<Output = Result<BlockHash>>;
@@ -31,3 +43,115 @@ pub trait ResolverApi {
/// Returns the number of the last block. /// Returns the number of the last block.
fn last_block_number(&self) -> impl Future<Output = Result<BlockNumber>>; fn last_block_number(&self) -> impl Future<Output = Result<BlockNumber>>;
} }
#[derive(Clone, Copy, Debug, Default)]
/// Contextual information required by the code that's performing the resolution.
pub struct ResolutionContext<'a> {
/// When provided the contracts provided here will be used for resolutions.
deployed_contracts: Option<&'a HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>>,
/// When provided the variables in here will be used for performing resolutions.
variables: Option<&'a HashMap<String, U256>>,
/// When provided this block number will be treated as the tip of the chain.
block_number: Option<&'a BlockNumber>,
/// When provided the resolver will use this transaction hash for all of its resolutions.
transaction_hash: Option<&'a TxHash>,
}
impl<'a> ResolutionContext<'a> {
pub fn new() -> Self {
Default::default()
}
pub fn new_from_parts(
deployed_contracts: impl Into<
Option<&'a HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>>,
>,
variables: impl Into<Option<&'a HashMap<String, U256>>>,
block_number: impl Into<Option<&'a BlockNumber>>,
transaction_hash: impl Into<Option<&'a TxHash>>,
) -> Self {
Self {
deployed_contracts: deployed_contracts.into(),
variables: variables.into(),
block_number: block_number.into(),
transaction_hash: transaction_hash.into(),
}
}
pub fn with_deployed_contracts(
mut self,
deployed_contracts: impl Into<
Option<&'a HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>>,
>,
) -> Self {
self.deployed_contracts = deployed_contracts.into();
self
}
pub fn with_variables(
mut self,
variables: impl Into<Option<&'a HashMap<String, U256>>>,
) -> Self {
self.variables = variables.into();
self
}
pub fn with_block_number(mut self, block_number: impl Into<Option<&'a BlockNumber>>) -> Self {
self.block_number = block_number.into();
self
}
pub fn with_transaction_hash(
mut self,
transaction_hash: impl Into<Option<&'a TxHash>>,
) -> Self {
self.transaction_hash = transaction_hash.into();
self
}
pub fn resolve_block_number(&self, number: BlockNumberOrTag) -> BlockNumberOrTag {
match self.block_number {
Some(block_number) => match number {
BlockNumberOrTag::Latest => BlockNumberOrTag::Number(*block_number),
n @ (BlockNumberOrTag::Finalized
| BlockNumberOrTag::Safe
| BlockNumberOrTag::Earliest
| BlockNumberOrTag::Pending
| BlockNumberOrTag::Number(_)) => n,
},
None => number,
}
}
pub fn deployed_contract(
&self,
instance: &ContractInstance,
) -> Option<&(ContractIdent, Address, JsonAbi)> {
self.deployed_contracts
.and_then(|deployed_contracts| deployed_contracts.get(instance))
}
pub fn deployed_contract_address(&self, instance: &ContractInstance) -> Option<&Address> {
self.deployed_contract(instance).map(|(_, a, _)| a)
}
pub fn deployed_contract_abi(&self, instance: &ContractInstance) -> Option<&JsonAbi> {
self.deployed_contract(instance).map(|(_, _, a)| a)
}
pub fn variable(&self, name: impl AsRef<str>) -> Option<&U256> {
self.variables
.and_then(|variables| variables.get(name.as_ref()))
}
pub fn tip_block_number(&self) -> Option<&'a BlockNumber> {
self.block_number
}
pub fn transaction_hash(&self) -> Option<&'a TxHash> {
self.transaction_hash
}
}
+3
View File
@@ -11,3 +11,6 @@ rust-version.workspace = true
[dependencies] [dependencies]
alloy = { workspace = true } alloy = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
[lints]
workspace = true
+12 -1
View File
@@ -1,7 +1,8 @@
//! This crate implements all node interactions. //! This crate implements all node interactions.
use alloy::primitives::{Address, StorageKey, U256};
use alloy::rpc::types::trace::geth::{DiffMode, GethDebugTracingOptions, GethTrace}; use alloy::rpc::types::trace::geth::{DiffMode, GethDebugTracingOptions, GethTrace};
use alloy::rpc::types::{TransactionReceipt, TransactionRequest}; use alloy::rpc::types::{EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest};
use anyhow::Result; use anyhow::Result;
/// An interface for all interactions with Ethereum compatible nodes. /// An interface for all interactions with Ethereum compatible nodes.
@@ -21,4 +22,14 @@ pub trait EthereumNode {
/// Returns the state diff of the transaction hash in the [TransactionReceipt]. /// Returns the state diff of the transaction hash in the [TransactionReceipt].
fn state_diff(&self, receipt: &TransactionReceipt) -> impl Future<Output = Result<DiffMode>>; fn state_diff(&self, receipt: &TransactionReceipt) -> impl Future<Output = Result<DiffMode>>;
/// Returns the balance of the provided [`Address`] back.
fn balance_of(&self, address: Address) -> impl Future<Output = Result<U256>>;
/// Returns the latest storage proof of the provided [`Address`]
fn latest_state_proof(
&self,
address: Address,
keys: Vec<StorageKey>,
) -> impl Future<Output = Result<EIP1186AccountProofResponse>>;
} }
+4
View File
@@ -14,6 +14,7 @@ alloy = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
revive-common = { workspace = true }
revive-dt-common = { workspace = true } revive-dt-common = { workspace = true }
revive-dt-config = { workspace = true } revive-dt-config = { workspace = true }
revive-dt-format = { workspace = true } revive-dt-format = { workspace = true }
@@ -28,3 +29,6 @@ sp-runtime = { workspace = true }
[dev-dependencies] [dev-dependencies]
temp-dir = { workspace = true } temp-dir = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
[lints]
workspace = true
+208 -125
View File
@@ -3,9 +3,13 @@
use std::{ use std::{
fs::{File, OpenOptions, create_dir_all, remove_dir_all}, fs::{File, OpenOptions, create_dir_all, remove_dir_all},
io::{BufRead, BufReader, Read, Write}, io::{BufRead, BufReader, Read, Write},
ops::ControlFlow,
path::PathBuf, path::PathBuf,
process::{Child, Command, Stdio}, process::{Child, Command, Stdio},
sync::atomic::{AtomicU32, Ordering}, sync::{
Arc,
atomic::{AtomicU32, Ordering},
},
time::{Duration, Instant}, time::{Duration, Instant},
}; };
@@ -13,23 +17,31 @@ use alloy::{
eips::BlockNumberOrTag, eips::BlockNumberOrTag,
genesis::{Genesis, GenesisAccount}, genesis::{Genesis, GenesisAccount},
network::{Ethereum, EthereumWallet, NetworkWallet}, network::{Ethereum, EthereumWallet, NetworkWallet},
primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, FixedBytes, U256}, primitives::{
Address, BlockHash, BlockNumber, BlockTimestamp, FixedBytes, StorageKey, TxHash, U256,
},
providers::{ providers::{
Provider, ProviderBuilder, Provider, ProviderBuilder,
ext::DebugApi, ext::DebugApi,
fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller}, fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller},
}, },
rpc::types::{ rpc::types::{
TransactionReceipt, TransactionRequest, EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest,
trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame}, trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame},
}, },
signers::local::PrivateKeySigner, signers::local::PrivateKeySigner,
}; };
use revive_dt_common::fs::clear_directory; use anyhow::Context;
use revive_common::EVMVersion;
use tracing::{Instrument, instrument};
use revive_dt_common::{
fs::clear_directory,
futures::{PollingWaitBehavior, poll},
};
use revive_dt_config::Arguments; use revive_dt_config::Arguments;
use revive_dt_format::traits::ResolverApi; use revive_dt_format::traits::ResolverApi;
use revive_dt_node_interaction::EthereumNode; use revive_dt_node_interaction::EthereumNode;
use tracing::Level;
use crate::{Node, common::FallbackGasFiller, constants::INITIAL_BALANCE}; use crate::{Node, common::FallbackGasFiller, constants::INITIAL_BALANCE};
@@ -43,6 +55,7 @@ static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
/// ///
/// Prunes the child process and the base directory on drop. /// Prunes the child process and the base directory on drop.
#[derive(Debug)] #[derive(Debug)]
#[allow(clippy::type_complexity)]
pub struct GethNode { pub struct GethNode {
connection_string: String, connection_string: String,
base_directory: PathBuf, base_directory: PathBuf,
@@ -51,10 +64,10 @@ pub struct GethNode {
geth: PathBuf, geth: PathBuf,
id: u32, id: u32,
handle: Option<Child>, handle: Option<Child>,
network_id: u64,
start_timeout: u64, start_timeout: u64,
wallet: EthereumWallet, wallet: Arc<EthereumWallet>,
nonce_manager: CachedNonceManager, nonce_manager: CachedNonceManager,
chain_id_filler: ChainIdFiller,
/// This vector stores [`File`] objects that we use for logging which we want to flush when the /// This vector stores [`File`] objects that we use for logging which we want to flush when the
/// node object is dropped. We do not store them in a structured fashion at the moment (in /// node object is dropped. We do not store them in a structured fashion at the moment (in
/// separate fields) as the logic that we need to apply to them is all the same regardless of /// separate fields) as the logic that we need to apply to them is all the same regardless of
@@ -77,9 +90,13 @@ impl GethNode {
const GETH_STDERR_LOG_FILE_NAME: &str = "node_stderr.log"; const GETH_STDERR_LOG_FILE_NAME: &str = "node_stderr.log";
const TRANSACTION_INDEXING_ERROR: &str = "transaction indexing is in progress"; const TRANSACTION_INDEXING_ERROR: &str = "transaction indexing is in progress";
const TRANSACTION_TRACING_ERROR: &str = "historical state not available in path scheme yet";
const RECEIPT_POLLING_DURATION: Duration = Duration::from_secs(5 * 60);
const TRACE_POLLING_DURATION: Duration = Duration::from_secs(60);
/// Create the node directory and call `geth init` to configure the genesis. /// Create the node directory and call `geth init` to configure the genesis.
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
fn init(&mut self, genesis: String) -> anyhow::Result<&mut Self> { fn init(&mut self, genesis: String) -> anyhow::Result<&mut Self> {
let _ = clear_directory(&self.base_directory); let _ = clear_directory(&self.base_directory);
let _ = clear_directory(&self.logs_directory); let _ = clear_directory(&self.logs_directory);
@@ -102,6 +119,8 @@ impl GethNode {
serde_json::to_writer(File::create(&genesis_path)?, &genesis)?; serde_json::to_writer(File::create(&genesis_path)?, &genesis)?;
let mut child = Command::new(&self.geth) let mut child = Command::new(&self.geth)
.arg("--state.scheme")
.arg("hash")
.arg("init") .arg("init")
.arg("--datadir") .arg("--datadir")
.arg(&self.data_directory) .arg(&self.data_directory)
@@ -127,7 +146,7 @@ impl GethNode {
/// Spawn the go-ethereum node child process. /// Spawn the go-ethereum node child process.
/// ///
/// [Instance::init] must be called prior. /// [Instance::init] must be called prior.
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
fn spawn_process(&mut self) -> anyhow::Result<&mut Self> { fn spawn_process(&mut self) -> anyhow::Result<&mut Self> {
// This is the `OpenOptions` that we wish to use for all of the log files that we will be // This is the `OpenOptions` that we wish to use for all of the log files that we will be
// opening in this method. We need to construct it in this way to: // opening in this method. We need to construct it in this way to:
@@ -150,8 +169,6 @@ impl GethNode {
.arg(&self.data_directory) .arg(&self.data_directory)
.arg("--ipcpath") .arg("--ipcpath")
.arg(&self.connection_string) .arg(&self.connection_string)
.arg("--networkid")
.arg(self.network_id.to_string())
.arg("--nodiscover") .arg("--nodiscover")
.arg("--maxpeers") .arg("--maxpeers")
.arg("0") .arg("0")
@@ -159,6 +176,12 @@ impl GethNode {
.arg("0") .arg("0")
.arg("--cache.blocklogs") .arg("--cache.blocklogs")
.arg("512") .arg("512")
.arg("--state.scheme")
.arg("hash")
.arg("--syncmode")
.arg("full")
.arg("--gcmode")
.arg("archive")
.stderr(stderr_logs_file.try_clone()?) .stderr(stderr_logs_file.try_clone()?)
.stdout(stdout_logs_file.try_clone()?) .stdout(stdout_logs_file.try_clone()?)
.spawn()? .spawn()?
@@ -179,7 +202,7 @@ impl GethNode {
/// Wait for the g-ethereum node child process getting ready. /// Wait for the g-ethereum node child process getting ready.
/// ///
/// [Instance::spawn_process] must be called priorly. /// [Instance::spawn_process] must be called priorly.
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
fn wait_ready(&mut self) -> anyhow::Result<&mut Self> { fn wait_ready(&mut self) -> anyhow::Result<&mut Self> {
let start_time = Instant::now(); let start_time = Instant::now();
@@ -192,6 +215,7 @@ impl GethNode {
let maximum_wait_time = Duration::from_millis(self.start_timeout); let maximum_wait_time = Duration::from_millis(self.start_timeout);
let mut stderr = BufReader::new(logs_file).lines(); let mut stderr = BufReader::new(logs_file).lines();
let mut lines = vec![];
loop { loop {
if let Some(Ok(line)) = stderr.next() { if let Some(Ok(line)) = stderr.next() {
if line.contains(Self::ERROR_MARKER) { if line.contains(Self::ERROR_MARKER) {
@@ -200,142 +224,145 @@ impl GethNode {
if line.contains(Self::READY_MARKER) { if line.contains(Self::READY_MARKER) {
return Ok(self); return Ok(self);
} }
lines.push(line);
} }
if Instant::now().duration_since(start_time) > maximum_wait_time { if Instant::now().duration_since(start_time) > maximum_wait_time {
anyhow::bail!("Timeout in starting geth"); anyhow::bail!(
"Timeout in starting geth: took longer than {}ms. stdout:\n\n{}\n",
self.start_timeout,
lines.join("\n")
);
} }
} }
} }
#[tracing::instrument(skip_all, fields(geth_node_id = self.id), level = Level::TRACE)] #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
fn geth_stdout_log_file_path(&self) -> PathBuf { fn geth_stdout_log_file_path(&self) -> PathBuf {
self.logs_directory.join(Self::GETH_STDOUT_LOG_FILE_NAME) self.logs_directory.join(Self::GETH_STDOUT_LOG_FILE_NAME)
} }
#[tracing::instrument(skip_all, fields(geth_node_id = self.id), level = Level::TRACE)] #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
fn geth_stderr_log_file_path(&self) -> PathBuf { fn geth_stderr_log_file_path(&self) -> PathBuf {
self.logs_directory.join(Self::GETH_STDERR_LOG_FILE_NAME) self.logs_directory.join(Self::GETH_STDERR_LOG_FILE_NAME)
} }
fn provider( async fn provider(
&self, &self,
) -> impl Future< ) -> anyhow::Result<FillProvider<impl TxFiller<Ethereum>, impl Provider<Ethereum>, Ethereum>>
Output = anyhow::Result< {
FillProvider<impl TxFiller<Ethereum>, impl Provider<Ethereum>, Ethereum>, ProviderBuilder::new()
>, .disable_recommended_fillers()
> + 'static { .filler(FallbackGasFiller::new(
let connection_string = self.connection_string(); 25_000_000,
let wallet = self.wallet.clone(); 1_000_000_000,
1_000_000_000,
// Note: We would like all providers to make use of the same nonce manager so that we have ))
// monotonically increasing nonces that are cached. The cached nonce manager uses Arc's in .filler(self.chain_id_filler.clone())
// its implementation and therefore it means that when we clone it then it still references .filler(NonceFiller::new(self.nonce_manager.clone()))
// the same state. .wallet(self.wallet.clone())
let nonce_manager = self.nonce_manager.clone(); .connect(&self.connection_string)
.await
Box::pin(async move { .map_err(Into::into)
ProviderBuilder::new()
.disable_recommended_fillers()
.filler(FallbackGasFiller::new(500_000_000, 500_000_000, 1))
.filler(ChainIdFiller::default())
.filler(NonceFiller::new(nonce_manager))
.wallet(wallet)
.connect(&connection_string)
.await
.map_err(Into::into)
})
} }
} }
impl EthereumNode for GethNode { impl EthereumNode for GethNode {
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))] #[instrument(
level = "info",
skip_all,
fields(geth_node_id = self.id, connection_string = self.connection_string),
err,
)]
async fn execute_transaction( async fn execute_transaction(
&self, &self,
transaction: TransactionRequest, transaction: TransactionRequest,
) -> anyhow::Result<alloy::rpc::types::TransactionReceipt> { ) -> anyhow::Result<alloy::rpc::types::TransactionReceipt> {
let outer_span = tracing::debug_span!("Submitting transaction", ?transaction);
let _outer_guard = outer_span.enter();
let provider = self.provider().await?; let provider = self.provider().await?;
let pending_transaction = provider.send_transaction(transaction).await?; let pending_transaction = provider.send_transaction(transaction).await.inspect_err(
let transaction_hash = pending_transaction.tx_hash(); |err| tracing::error!(%err, "Encountered an error when submitting the transaction"),
)?;
let transaction_hash = *pending_transaction.tx_hash();
let span = tracing::info_span!("Awaiting transaction receipt", ?transaction_hash); // The following is a fix for the "transaction indexing is in progress" error that we used
let _guard = span.enter(); // to get. You can find more information on this in the following GH issue in geth
// The following is a fix for the "transaction indexing is in progress" error that we
// used to get. You can find more information on this in the following GH issue in geth
// https://github.com/ethereum/go-ethereum/issues/28877. To summarize what's going on, // https://github.com/ethereum/go-ethereum/issues/28877. To summarize what's going on,
// before we can get the receipt of the transaction it needs to have been indexed by the // before we can get the receipt of the transaction it needs to have been indexed by the
// node's indexer. Just because the transaction has been confirmed it doesn't mean that // node's indexer. Just because the transaction has been confirmed it doesn't mean that it
// it has been indexed. When we call alloy's `get_receipt` it checks if the transaction // has been indexed. When we call alloy's `get_receipt` it checks if the transaction was
// was confirmed. If it has been, then it will call `eth_getTransactionReceipt` method // confirmed. If it has been, then it will call `eth_getTransactionReceipt` method which
// which _might_ return the above error if the tx has not yet been indexed yet. So, we // _might_ return the above error if the tx has not yet been indexed yet. So, we need to
// need to implement a retry mechanism for the receipt to keep retrying to get it until // implement a retry mechanism for the receipt to keep retrying to get it until it
// it eventually works, but we only do that if the error we get back is the "transaction // eventually works, but we only do that if the error we get back is the "transaction
// indexing is in progress" error or if the receipt is None. // indexing is in progress" error or if the receipt is None.
// //
// Getting the transaction indexed and taking a receipt can take a long time especially // Getting the transaction indexed and taking a receipt can take a long time especially when
// when a lot of transactions are being submitted to the node. Thus, while initially we // a lot of transactions are being submitted to the node. Thus, while initially we only
// only allowed for 60 seconds of waiting with a 1 second delay in polling, we need to // allowed for 60 seconds of waiting with a 1 second delay in polling, we need to allow for
// allow for a larger wait time. Therefore, in here we allow for 5 minutes of waiting // a larger wait time. Therefore, in here we allow for 5 minutes of waiting with exponential
// with exponential backoff each time we attempt to get the receipt and find that it's // backoff each time we attempt to get the receipt and find that it's not available.
// not available. let provider = Arc::new(provider);
let mut retries = 0; poll(
let mut total_wait_duration = Duration::from_secs(0); Self::RECEIPT_POLLING_DURATION,
let max_allowed_wait_duration = Duration::from_secs(5 * 60); PollingWaitBehavior::Constant(Duration::from_millis(200)),
loop { move || {
if total_wait_duration >= max_allowed_wait_duration { let provider = provider.clone();
tracing::error!( async move {
?total_wait_duration, match provider.get_transaction_receipt(transaction_hash).await {
?max_allowed_wait_duration, Ok(Some(receipt)) => Ok(ControlFlow::Break(receipt)),
retry_count = retries, Ok(None) => Ok(ControlFlow::Continue(())),
"Failed to get receipt after polling for it" Err(error) => {
); let error_string = error.to_string();
anyhow::bail!( match error_string.contains(Self::TRANSACTION_INDEXING_ERROR) {
"Polled for receipt for {total_wait_duration:?} but failed to get it" true => Ok(ControlFlow::Continue(())),
); false => Err(error.into()),
} }
}
match provider.get_transaction_receipt(*transaction_hash).await {
Ok(Some(receipt)) => {
tracing::info!(?total_wait_duration, "Found receipt");
break Ok(receipt);
}
Ok(None) => {}
Err(error) => {
let error_string = error.to_string();
if !error_string.contains(Self::TRANSACTION_INDEXING_ERROR) {
break Err(error.into());
} }
} }
}; },
)
let next_wait_duration = Duration::from_secs(2u64.pow(retries)) .instrument(tracing::info_span!(
.min(max_allowed_wait_duration - total_wait_duration); "Awaiting transaction receipt",
total_wait_duration += next_wait_duration; ?transaction_hash
retries += 1; ))
.await
tokio::time::sleep(next_wait_duration).await;
}
} }
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
async fn trace_transaction( async fn trace_transaction(
&self, &self,
transaction: &TransactionReceipt, transaction: &TransactionReceipt,
trace_options: GethDebugTracingOptions, trace_options: GethDebugTracingOptions,
) -> anyhow::Result<alloy::rpc::types::trace::geth::GethTrace> { ) -> anyhow::Result<alloy::rpc::types::trace::geth::GethTrace> {
let tx_hash = transaction.transaction_hash; let provider = Arc::new(self.provider().await?);
Ok(self poll(
.provider() Self::TRACE_POLLING_DURATION,
.await? PollingWaitBehavior::Constant(Duration::from_millis(200)),
.debug_trace_transaction(tx_hash, trace_options) move || {
.await?) let provider = provider.clone();
let trace_options = trace_options.clone();
async move {
match provider
.debug_trace_transaction(transaction.transaction_hash, trace_options)
.await
{
Ok(trace) => Ok(ControlFlow::Break(trace)),
Err(error) => {
let error_string = error.to_string();
match error_string.contains(Self::TRANSACTION_TRACING_ERROR) {
true => Ok(ControlFlow::Continue(())),
false => Err(error.into()),
}
}
}
}
},
)
.await
} }
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
async fn state_diff(&self, transaction: &TransactionReceipt) -> anyhow::Result<DiffMode> { async fn state_diff(&self, transaction: &TransactionReceipt) -> anyhow::Result<DiffMode> {
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig { let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
diff_mode: Some(true), diff_mode: Some(true),
@@ -351,10 +378,33 @@ impl EthereumNode for GethNode {
_ => anyhow::bail!("expected a diff mode trace"), _ => anyhow::bail!("expected a diff mode trace"),
} }
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
async fn balance_of(&self, address: Address) -> anyhow::Result<U256> {
self.provider()
.await?
.get_balance(address)
.await
.map_err(Into::into)
}
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
async fn latest_state_proof(
&self,
address: Address,
keys: Vec<StorageKey>,
) -> anyhow::Result<EIP1186AccountProofResponse> {
self.provider()
.await?
.get_proof(address, keys)
.latest()
.await
.map_err(Into::into)
}
} }
impl ResolverApi for GethNode { impl ResolverApi for GethNode {
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
async fn chain_id(&self) -> anyhow::Result<alloy::primitives::ChainId> { async fn chain_id(&self) -> anyhow::Result<alloy::primitives::ChainId> {
self.provider() self.provider()
.await? .await?
@@ -363,7 +413,17 @@ impl ResolverApi for GethNode {
.map_err(Into::into) .map_err(Into::into)
} }
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
async fn transaction_gas_price(&self, tx_hash: &TxHash) -> anyhow::Result<u128> {
self.provider()
.await?
.get_transaction_receipt(*tx_hash)
.await?
.context("Failed to get the transaction receipt")
.map(|receipt| receipt.effective_gas_price)
}
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
async fn block_gas_limit(&self, number: BlockNumberOrTag) -> anyhow::Result<u128> { async fn block_gas_limit(&self, number: BlockNumberOrTag) -> anyhow::Result<u128> {
self.provider() self.provider()
.await? .await?
@@ -373,7 +433,7 @@ impl ResolverApi for GethNode {
.map(|block| block.header.gas_limit as _) .map(|block| block.header.gas_limit as _)
} }
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
async fn block_coinbase(&self, number: BlockNumberOrTag) -> anyhow::Result<Address> { async fn block_coinbase(&self, number: BlockNumberOrTag) -> anyhow::Result<Address> {
self.provider() self.provider()
.await? .await?
@@ -383,17 +443,32 @@ impl ResolverApi for GethNode {
.map(|block| block.header.beneficiary) .map(|block| block.header.beneficiary)
} }
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
async fn block_difficulty(&self, number: BlockNumberOrTag) -> anyhow::Result<U256> { async fn block_difficulty(&self, number: BlockNumberOrTag) -> anyhow::Result<U256> {
self.provider() self.provider()
.await? .await?
.get_block_by_number(number) .get_block_by_number(number)
.await? .await?
.ok_or(anyhow::Error::msg("Blockchain has no blocks")) .ok_or(anyhow::Error::msg("Blockchain has no blocks"))
.map(|block| block.header.difficulty) .map(|block| U256::from_be_bytes(block.header.mix_hash.0))
} }
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
async fn block_base_fee(&self, number: BlockNumberOrTag) -> anyhow::Result<u64> {
self.provider()
.await?
.get_block_by_number(number)
.await?
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
.and_then(|block| {
block
.header
.base_fee_per_gas
.context("Failed to get the base fee per gas")
})
}
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
async fn block_hash(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockHash> { async fn block_hash(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockHash> {
self.provider() self.provider()
.await? .await?
@@ -403,7 +478,7 @@ impl ResolverApi for GethNode {
.map(|block| block.header.hash) .map(|block| block.header.hash)
} }
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
async fn block_timestamp(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockTimestamp> { async fn block_timestamp(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockTimestamp> {
self.provider() self.provider()
.await? .await?
@@ -413,7 +488,7 @@ impl ResolverApi for GethNode {
.map(|block| block.header.timestamp) .map(|block| block.header.timestamp)
} }
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
async fn last_block_number(&self) -> anyhow::Result<BlockNumber> { async fn last_block_number(&self) -> anyhow::Result<BlockNumber> {
self.provider() self.provider()
.await? .await?
@@ -446,22 +521,27 @@ impl Node for GethNode {
geth: config.geth.clone(), geth: config.geth.clone(),
id, id,
handle: None, handle: None,
network_id: config.network_id,
start_timeout: config.geth_start_timeout, start_timeout: config.geth_start_timeout,
wallet, wallet: Arc::new(wallet),
chain_id_filler: Default::default(),
nonce_manager: Default::default(),
// We know that we only need to be storing 2 files so we can specify that when creating // We know that we only need to be storing 2 files so we can specify that when creating
// the vector. It's the stdout and stderr of the geth node. // the vector. It's the stdout and stderr of the geth node.
logs_file_to_flush: Vec::with_capacity(2), logs_file_to_flush: Vec::with_capacity(2),
nonce_manager: Default::default(),
} }
} }
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
fn id(&self) -> usize {
self.id as _
}
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
fn connection_string(&self) -> String { fn connection_string(&self) -> String {
self.connection_string.clone() self.connection_string.clone()
} }
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
fn shutdown(&mut self) -> anyhow::Result<()> { fn shutdown(&mut self) -> anyhow::Result<()> {
// Terminate the processes in a graceful manner to allow for the output to be flushed. // Terminate the processes in a graceful manner to allow for the output to be flushed.
if let Some(mut child) = self.handle.take() { if let Some(mut child) = self.handle.take() {
@@ -483,13 +563,13 @@ impl Node for GethNode {
Ok(()) Ok(())
} }
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
fn spawn(&mut self, genesis: String) -> anyhow::Result<()> { fn spawn(&mut self, genesis: String) -> anyhow::Result<()> {
self.init(genesis)?.spawn_process()?; self.init(genesis)?.spawn_process()?;
Ok(()) Ok(())
} }
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
fn version(&self) -> anyhow::Result<String> { fn version(&self) -> anyhow::Result<String> {
let output = Command::new(&self.geth) let output = Command::new(&self.geth)
.arg("--version") .arg("--version")
@@ -502,17 +582,20 @@ impl Node for GethNode {
Ok(String::from_utf8_lossy(&output).into()) Ok(String::from_utf8_lossy(&output).into())
} }
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))] fn matches_target(targets: Option<&[String]>) -> bool {
fn matches_target(&self, targets: Option<&[String]>) -> bool {
match targets { match targets {
None => true, None => true,
Some(targets) => targets.iter().any(|str| str.as_str() == "evm"), Some(targets) => targets.iter().any(|str| str.as_str() == "evm"),
} }
} }
fn evm_version() -> EVMVersion {
EVMVersion::Cancun
}
} }
impl Drop for GethNode { impl Drop for GethNode {
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
fn drop(&mut self) { fn drop(&mut self) {
self.shutdown().expect("Failed to shutdown") self.shutdown().expect("Failed to shutdown")
} }
+120 -81
View File
@@ -3,7 +3,10 @@ use std::{
io::{BufRead, Write}, io::{BufRead, Write},
path::{Path, PathBuf}, path::{Path, PathBuf},
process::{Child, Command, Stdio}, process::{Child, Command, Stdio},
sync::atomic::{AtomicU32, Ordering}, sync::{
Arc,
atomic::{AtomicU32, Ordering},
},
time::Duration, time::Duration,
}; };
@@ -16,7 +19,8 @@ use alloy::{
TransactionBuilderError, UnbuiltTransactionError, TransactionBuilderError, UnbuiltTransactionError,
}, },
primitives::{ primitives::{
Address, B64, B256, BlockHash, BlockNumber, BlockTimestamp, Bloom, Bytes, FixedBytes, U256, Address, B64, B256, BlockHash, BlockNumber, BlockTimestamp, Bloom, Bytes, FixedBytes,
StorageKey, TxHash, U256,
}, },
providers::{ providers::{
Provider, ProviderBuilder, Provider, ProviderBuilder,
@@ -24,19 +28,20 @@ use alloy::{
fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller}, fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller},
}, },
rpc::types::{ rpc::types::{
TransactionReceipt, EIP1186AccountProofResponse, TransactionReceipt,
eth::{Block, Header, Transaction}, eth::{Block, Header, Transaction},
trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame}, trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame},
}, },
signers::local::PrivateKeySigner, signers::local::PrivateKeySigner,
}; };
use anyhow::Context;
use revive_common::EVMVersion;
use revive_dt_common::fs::clear_directory; use revive_dt_common::fs::clear_directory;
use revive_dt_format::traits::ResolverApi; use revive_dt_format::traits::ResolverApi;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{Value as JsonValue, json}; use serde_json::{Value as JsonValue, json};
use sp_core::crypto::Ss58Codec; use sp_core::crypto::Ss58Codec;
use sp_runtime::AccountId32; use sp_runtime::AccountId32;
use tracing::Level;
use revive_dt_config::Arguments; use revive_dt_config::Arguments;
use revive_dt_node_interaction::EthereumNode; use revive_dt_node_interaction::EthereumNode;
@@ -49,14 +54,17 @@ static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
pub struct KitchensinkNode { pub struct KitchensinkNode {
id: u32, id: u32,
substrate_binary: PathBuf, substrate_binary: PathBuf,
dev_node_binary: PathBuf,
eth_proxy_binary: PathBuf, eth_proxy_binary: PathBuf,
rpc_url: String, rpc_url: String,
wallet: EthereumWallet,
base_directory: PathBuf, base_directory: PathBuf,
logs_directory: PathBuf, logs_directory: PathBuf,
process_substrate: Option<Child>, process_substrate: Option<Child>,
process_proxy: Option<Child>, process_proxy: Option<Child>,
wallet: Arc<EthereumWallet>,
nonce_manager: CachedNonceManager, nonce_manager: CachedNonceManager,
chain_id_filler: ChainIdFiller,
use_kitchensink_not_dev_node: bool,
/// This vector stores [`File`] objects that we use for logging which we want to flush when the /// This vector stores [`File`] objects that we use for logging which we want to flush when the
/// node object is dropped. We do not store them in a structured fashion at the moment (in /// node object is dropped. We do not store them in a structured fashion at the moment (in
/// separate fields) as the logic that we need to apply to them is all the same regardless of /// separate fields) as the logic that we need to apply to them is all the same regardless of
@@ -84,7 +92,6 @@ impl KitchensinkNode {
const PROXY_STDOUT_LOG_FILE_NAME: &str = "proxy_stdout.log"; const PROXY_STDOUT_LOG_FILE_NAME: &str = "proxy_stdout.log";
const PROXY_STDERR_LOG_FILE_NAME: &str = "proxy_stderr.log"; const PROXY_STDERR_LOG_FILE_NAME: &str = "proxy_stderr.log";
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn init(&mut self, genesis: &str) -> anyhow::Result<&mut Self> { fn init(&mut self, genesis: &str) -> anyhow::Result<&mut Self> {
let _ = clear_directory(&self.base_directory); let _ = clear_directory(&self.base_directory);
let _ = clear_directory(&self.logs_directory); let _ = clear_directory(&self.logs_directory);
@@ -96,11 +103,21 @@ impl KitchensinkNode {
// Note: we do not pipe the logs of this process to a separate file since this is just a // Note: we do not pipe the logs of this process to a separate file since this is just a
// once-off export of the default chain spec and not part of the long-running node process. // once-off export of the default chain spec and not part of the long-running node process.
let output = Command::new(&self.substrate_binary) let output = if self.use_kitchensink_not_dev_node {
.arg("export-chain-spec") Command::new(&self.substrate_binary)
.arg("--chain") .arg("export-chain-spec")
.arg("dev") .arg("--chain")
.output()?; .arg("dev")
.output()
.context("Failed to export the chain-spec")?
} else {
Command::new(&self.dev_node_binary)
.arg("build-spec")
.arg("--chain")
.arg("dev")
.output()
.context("Failed to export the chain-spec")?
};
if !output.status.success() { if !output.status.success() {
anyhow::bail!( anyhow::bail!(
@@ -157,7 +174,6 @@ impl KitchensinkNode {
Ok(self) Ok(self)
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn spawn_process(&mut self) -> anyhow::Result<()> { fn spawn_process(&mut self) -> anyhow::Result<()> {
let substrate_rpc_port = Self::BASE_SUBSTRATE_RPC_PORT + self.id as u16; let substrate_rpc_port = Self::BASE_SUBSTRATE_RPC_PORT + self.id as u16;
let proxy_rpc_port = Self::BASE_PROXY_RPC_PORT + self.id as u16; let proxy_rpc_port = Self::BASE_PROXY_RPC_PORT + self.id as u16;
@@ -184,7 +200,12 @@ impl KitchensinkNode {
let kitchensink_stderr_logs_file = open_options let kitchensink_stderr_logs_file = open_options
.clone() .clone()
.open(self.kitchensink_stderr_log_file_path())?; .open(self.kitchensink_stderr_log_file_path())?;
self.process_substrate = Command::new(&self.substrate_binary) let node_binary_path = if self.use_kitchensink_not_dev_node {
self.substrate_binary.as_path()
} else {
self.dev_node_binary.as_path()
};
self.process_substrate = Command::new(node_binary_path)
.arg("--dev") .arg("--dev")
.arg("--chain") .arg("--chain")
.arg(chainspec_path) .arg(chainspec_path)
@@ -199,6 +220,8 @@ impl KitchensinkNode {
.arg("Unsafe") .arg("Unsafe")
.arg("--rpc-cors") .arg("--rpc-cors")
.arg("all") .arg("all")
.arg("--rpc-max-connections")
.arg(u32::MAX.to_string())
.env("RUST_LOG", Self::SUBSTRATE_LOG_ENV) .env("RUST_LOG", Self::SUBSTRATE_LOG_ENV)
.stdout(kitchensink_stdout_logs_file.try_clone()?) .stdout(kitchensink_stdout_logs_file.try_clone()?)
.stderr(kitchensink_stderr_logs_file.try_clone()?) .stderr(kitchensink_stderr_logs_file.try_clone()?)
@@ -209,12 +232,8 @@ impl KitchensinkNode {
if let Err(error) = Self::wait_ready( if let Err(error) = Self::wait_ready(
self.kitchensink_stderr_log_file_path().as_path(), self.kitchensink_stderr_log_file_path().as_path(),
Self::SUBSTRATE_READY_MARKER, Self::SUBSTRATE_READY_MARKER,
Duration::from_secs(30), Duration::from_secs(60),
) { ) {
tracing::error!(
?error,
"Failed to start substrate, shutting down gracefully"
);
self.shutdown()?; self.shutdown()?;
return Err(error); return Err(error);
}; };
@@ -229,6 +248,8 @@ impl KitchensinkNode {
.arg(proxy_rpc_port.to_string()) .arg(proxy_rpc_port.to_string())
.arg("--node-rpc-url") .arg("--node-rpc-url")
.arg(format!("ws://127.0.0.1:{substrate_rpc_port}")) .arg(format!("ws://127.0.0.1:{substrate_rpc_port}"))
.arg("--rpc-max-connections")
.arg(u32::MAX.to_string())
.env("RUST_LOG", Self::PROXY_LOG_ENV) .env("RUST_LOG", Self::PROXY_LOG_ENV)
.stdout(eth_proxy_stdout_logs_file.try_clone()?) .stdout(eth_proxy_stdout_logs_file.try_clone()?)
.stderr(eth_proxy_stderr_logs_file.try_clone()?) .stderr(eth_proxy_stderr_logs_file.try_clone()?)
@@ -238,9 +259,8 @@ impl KitchensinkNode {
if let Err(error) = Self::wait_ready( if let Err(error) = Self::wait_ready(
self.proxy_stderr_log_file_path().as_path(), self.proxy_stderr_log_file_path().as_path(),
Self::ETH_PROXY_READY_MARKER, Self::ETH_PROXY_READY_MARKER,
Duration::from_secs(30), Duration::from_secs(60),
) { ) {
tracing::error!(?error, "Failed to start proxy, shutting down gracefully");
self.shutdown()?; self.shutdown()?;
return Err(error); return Err(error);
}; };
@@ -255,7 +275,6 @@ impl KitchensinkNode {
Ok(()) Ok(())
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn extract_balance_from_genesis_file( fn extract_balance_from_genesis_file(
&self, &self,
genesis: &Genesis, genesis: &Genesis,
@@ -304,7 +323,6 @@ impl KitchensinkNode {
} }
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
pub fn eth_rpc_version(&self) -> anyhow::Result<String> { pub fn eth_rpc_version(&self) -> anyhow::Result<String> {
let output = Command::new(&self.eth_proxy_binary) let output = Command::new(&self.eth_proxy_binary)
.arg("--version") .arg("--version")
@@ -317,74 +335,55 @@ impl KitchensinkNode {
Ok(String::from_utf8_lossy(&output).trim().to_string()) Ok(String::from_utf8_lossy(&output).trim().to_string())
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), level = Level::TRACE)]
fn kitchensink_stdout_log_file_path(&self) -> PathBuf { fn kitchensink_stdout_log_file_path(&self) -> PathBuf {
self.logs_directory self.logs_directory
.join(Self::KITCHENSINK_STDOUT_LOG_FILE_NAME) .join(Self::KITCHENSINK_STDOUT_LOG_FILE_NAME)
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), level = Level::TRACE)]
fn kitchensink_stderr_log_file_path(&self) -> PathBuf { fn kitchensink_stderr_log_file_path(&self) -> PathBuf {
self.logs_directory self.logs_directory
.join(Self::KITCHENSINK_STDERR_LOG_FILE_NAME) .join(Self::KITCHENSINK_STDERR_LOG_FILE_NAME)
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), level = Level::TRACE)]
fn proxy_stdout_log_file_path(&self) -> PathBuf { fn proxy_stdout_log_file_path(&self) -> PathBuf {
self.logs_directory.join(Self::PROXY_STDOUT_LOG_FILE_NAME) self.logs_directory.join(Self::PROXY_STDOUT_LOG_FILE_NAME)
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), level = Level::TRACE)]
fn proxy_stderr_log_file_path(&self) -> PathBuf { fn proxy_stderr_log_file_path(&self) -> PathBuf {
self.logs_directory.join(Self::PROXY_STDERR_LOG_FILE_NAME) self.logs_directory.join(Self::PROXY_STDERR_LOG_FILE_NAME)
} }
fn provider( async fn provider(
&self, &self,
) -> impl Future< ) -> anyhow::Result<
Output = anyhow::Result< FillProvider<
FillProvider< impl TxFiller<KitchenSinkNetwork>,
impl TxFiller<KitchenSinkNetwork>, impl Provider<KitchenSinkNetwork>,
impl Provider<KitchenSinkNetwork>, KitchenSinkNetwork,
KitchenSinkNetwork,
>,
>, >,
> + 'static { > {
let connection_string = self.connection_string(); ProviderBuilder::new()
let wallet = self.wallet.clone(); .disable_recommended_fillers()
.network::<KitchenSinkNetwork>()
// Note: We would like all providers to make use of the same nonce manager so that we have .filler(FallbackGasFiller::new(
// monotonically increasing nonces that are cached. The cached nonce manager uses Arc's in 25_000_000,
// its implementation and therefore it means that when we clone it then it still references 1_000_000_000,
// the same state. 1_000_000_000,
let nonce_manager = self.nonce_manager.clone(); ))
.filler(self.chain_id_filler.clone())
Box::pin(async move { .filler(NonceFiller::new(self.nonce_manager.clone()))
ProviderBuilder::new() .wallet(self.wallet.clone())
.disable_recommended_fillers() .connect(&self.rpc_url)
.network::<KitchenSinkNetwork>() .await
.filler(FallbackGasFiller::new( .map_err(Into::into)
30_000_000,
200_000_000_000,
3_000_000_000,
))
.filler(ChainIdFiller::default())
.filler(NonceFiller::new(nonce_manager))
.wallet(wallet)
.connect(&connection_string)
.await
.map_err(Into::into)
})
} }
} }
impl EthereumNode for KitchensinkNode { impl EthereumNode for KitchensinkNode {
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
async fn execute_transaction( async fn execute_transaction(
&self, &self,
transaction: alloy::rpc::types::TransactionRequest, transaction: alloy::rpc::types::TransactionRequest,
) -> anyhow::Result<TransactionReceipt> { ) -> anyhow::Result<TransactionReceipt> {
tracing::debug!(?transaction, "Submitting transaction");
let receipt = self let receipt = self
.provider() .provider()
.await? .await?
@@ -392,11 +391,9 @@ impl EthereumNode for KitchensinkNode {
.await? .await?
.get_receipt() .get_receipt()
.await?; .await?;
tracing::info!(?receipt, "Submitted tx to kitchensink");
Ok(receipt) Ok(receipt)
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
async fn trace_transaction( async fn trace_transaction(
&self, &self,
transaction: &TransactionReceipt, transaction: &TransactionReceipt,
@@ -410,7 +407,6 @@ impl EthereumNode for KitchensinkNode {
.await?) .await?)
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
async fn state_diff(&self, transaction: &TransactionReceipt) -> anyhow::Result<DiffMode> { async fn state_diff(&self, transaction: &TransactionReceipt) -> anyhow::Result<DiffMode> {
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig { let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
diff_mode: Some(true), diff_mode: Some(true),
@@ -426,10 +422,30 @@ impl EthereumNode for KitchensinkNode {
_ => anyhow::bail!("expected a diff mode trace"), _ => anyhow::bail!("expected a diff mode trace"),
} }
} }
async fn balance_of(&self, address: Address) -> anyhow::Result<U256> {
self.provider()
.await?
.get_balance(address)
.await
.map_err(Into::into)
}
async fn latest_state_proof(
&self,
address: Address,
keys: Vec<StorageKey>,
) -> anyhow::Result<EIP1186AccountProofResponse> {
self.provider()
.await?
.get_proof(address, keys)
.latest()
.await
.map_err(Into::into)
}
} }
impl ResolverApi for KitchensinkNode { impl ResolverApi for KitchensinkNode {
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
async fn chain_id(&self) -> anyhow::Result<alloy::primitives::ChainId> { async fn chain_id(&self) -> anyhow::Result<alloy::primitives::ChainId> {
self.provider() self.provider()
.await? .await?
@@ -438,7 +454,15 @@ impl ResolverApi for KitchensinkNode {
.map_err(Into::into) .map_err(Into::into)
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))] async fn transaction_gas_price(&self, tx_hash: &TxHash) -> anyhow::Result<u128> {
self.provider()
.await?
.get_transaction_receipt(*tx_hash)
.await?
.context("Failed to get the transaction receipt")
.map(|receipt| receipt.effective_gas_price)
}
async fn block_gas_limit(&self, number: BlockNumberOrTag) -> anyhow::Result<u128> { async fn block_gas_limit(&self, number: BlockNumberOrTag) -> anyhow::Result<u128> {
self.provider() self.provider()
.await? .await?
@@ -448,7 +472,6 @@ impl ResolverApi for KitchensinkNode {
.map(|block| block.header.gas_limit as _) .map(|block| block.header.gas_limit as _)
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
async fn block_coinbase(&self, number: BlockNumberOrTag) -> anyhow::Result<Address> { async fn block_coinbase(&self, number: BlockNumberOrTag) -> anyhow::Result<Address> {
self.provider() self.provider()
.await? .await?
@@ -458,17 +481,29 @@ impl ResolverApi for KitchensinkNode {
.map(|block| block.header.beneficiary) .map(|block| block.header.beneficiary)
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
async fn block_difficulty(&self, number: BlockNumberOrTag) -> anyhow::Result<U256> { async fn block_difficulty(&self, number: BlockNumberOrTag) -> anyhow::Result<U256> {
self.provider() self.provider()
.await? .await?
.get_block_by_number(number) .get_block_by_number(number)
.await? .await?
.ok_or(anyhow::Error::msg("Blockchain has no blocks")) .ok_or(anyhow::Error::msg("Blockchain has no blocks"))
.map(|block| block.header.difficulty) .map(|block| U256::from_be_bytes(block.header.mix_hash.0))
}
async fn block_base_fee(&self, number: BlockNumberOrTag) -> anyhow::Result<u64> {
self.provider()
.await?
.get_block_by_number(number)
.await?
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
.and_then(|block| {
block
.header
.base_fee_per_gas
.context("Failed to get the base fee per gas")
})
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
async fn block_hash(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockHash> { async fn block_hash(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockHash> {
self.provider() self.provider()
.await? .await?
@@ -478,7 +513,6 @@ impl ResolverApi for KitchensinkNode {
.map(|block| block.header.hash) .map(|block| block.header.hash)
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
async fn block_timestamp(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockTimestamp> { async fn block_timestamp(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockTimestamp> {
self.provider() self.provider()
.await? .await?
@@ -488,7 +522,6 @@ impl ResolverApi for KitchensinkNode {
.map(|block| block.header.timestamp) .map(|block| block.header.timestamp)
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
async fn last_block_number(&self) -> anyhow::Result<BlockNumber> { async fn last_block_number(&self) -> anyhow::Result<BlockNumber> {
self.provider() self.provider()
.await? .await?
@@ -517,26 +550,31 @@ impl Node for KitchensinkNode {
Self { Self {
id, id,
substrate_binary: config.kitchensink.clone(), substrate_binary: config.kitchensink.clone(),
dev_node_binary: config.revive_dev_node.clone(),
eth_proxy_binary: config.eth_proxy.clone(), eth_proxy_binary: config.eth_proxy.clone(),
rpc_url: String::new(), rpc_url: String::new(),
wallet,
base_directory, base_directory,
logs_directory, logs_directory,
process_substrate: None, process_substrate: None,
process_proxy: None, process_proxy: None,
wallet: Arc::new(wallet),
chain_id_filler: Default::default(),
nonce_manager: Default::default(), nonce_manager: Default::default(),
use_kitchensink_not_dev_node: config.use_kitchensink_not_dev_node,
// We know that we only need to be storing 4 files so we can specify that when creating // We know that we only need to be storing 4 files so we can specify that when creating
// the vector. It's the stdout and stderr of the substrate-node and the eth-rpc. // the vector. It's the stdout and stderr of the substrate-node and the eth-rpc.
logs_file_to_flush: Vec::with_capacity(4), logs_file_to_flush: Vec::with_capacity(4),
} }
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))] fn id(&self) -> usize {
self.id as _
}
fn connection_string(&self) -> String { fn connection_string(&self) -> String {
self.rpc_url.clone() self.rpc_url.clone()
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn shutdown(&mut self) -> anyhow::Result<()> { fn shutdown(&mut self) -> anyhow::Result<()> {
// Terminate the processes in a graceful manner to allow for the output to be flushed. // Terminate the processes in a graceful manner to allow for the output to be flushed.
if let Some(mut child) = self.process_proxy.take() { if let Some(mut child) = self.process_proxy.take() {
@@ -563,12 +601,10 @@ impl Node for KitchensinkNode {
Ok(()) Ok(())
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn spawn(&mut self, genesis: String) -> anyhow::Result<()> { fn spawn(&mut self, genesis: String) -> anyhow::Result<()> {
self.init(&genesis)?.spawn_process() self.init(&genesis)?.spawn_process()
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn version(&self) -> anyhow::Result<String> { fn version(&self) -> anyhow::Result<String> {
let output = Command::new(&self.substrate_binary) let output = Command::new(&self.substrate_binary)
.arg("--version") .arg("--version")
@@ -581,17 +617,19 @@ impl Node for KitchensinkNode {
Ok(String::from_utf8_lossy(&output).into()) Ok(String::from_utf8_lossy(&output).into())
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))] fn matches_target(targets: Option<&[String]>) -> bool {
fn matches_target(&self, targets: Option<&[String]>) -> bool {
match targets { match targets {
None => true, None => true,
Some(targets) => targets.iter().any(|str| str.as_str() == "pvm"), Some(targets) => targets.iter().any(|str| str.as_str() == "pvm"),
} }
} }
fn evm_version() -> EVMVersion {
EVMVersion::Cancun
}
} }
impl Drop for KitchensinkNode { impl Drop for KitchensinkNode {
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn drop(&mut self) { fn drop(&mut self) {
self.shutdown().expect("Failed to shutdown") self.shutdown().expect("Failed to shutdown")
} }
@@ -1040,6 +1078,7 @@ mod tests {
Arguments { Arguments {
kitchensink: PathBuf::from("substrate-node"), kitchensink: PathBuf::from("substrate-node"),
eth_proxy: PathBuf::from("eth-rpc"), eth_proxy: PathBuf::from("eth-rpc"),
use_kitchensink_not_dev_node: true,
..Default::default() ..Default::default()
} }
} }
+8 -1
View File
@@ -1,5 +1,6 @@
//! This crate implements the testing nodes. //! This crate implements the testing nodes.
use revive_common::EVMVersion;
use revive_dt_config::Arguments; use revive_dt_config::Arguments;
use revive_dt_node_interaction::EthereumNode; use revive_dt_node_interaction::EthereumNode;
@@ -17,6 +18,9 @@ pub trait Node: EthereumNode {
/// Create a new uninitialized instance. /// Create a new uninitialized instance.
fn new(config: &Arguments) -> Self; fn new(config: &Arguments) -> Self;
/// Returns the identifier of the node.
fn id(&self) -> usize;
/// Spawns a node configured according to the genesis json. /// Spawns a node configured according to the genesis json.
/// ///
/// Blocking until it's ready to accept transactions. /// Blocking until it's ready to accept transactions.
@@ -35,5 +39,8 @@ pub trait Node: EthereumNode {
/// Given a list of targets from the metadata file, this function determines if the metadata /// Given a list of targets from the metadata file, this function determines if the metadata
/// file can be ran on this node or not. /// file can be ran on this node or not.
fn matches_target(&self, targets: Option<&[String]>) -> bool; fn matches_target(targets: Option<&[String]>) -> bool;
/// Returns the EVM version of the node.
fn evm_version() -> EVMVersion;
} }
+13 -2
View File
@@ -1,13 +1,15 @@
//! This crate implements concurrent handling of testing node. //! This crate implements concurrent handling of testing node.
use std::{ use std::{
fs::read_to_string,
sync::atomic::{AtomicUsize, Ordering}, sync::atomic::{AtomicUsize, Ordering},
thread, thread,
}; };
use revive_dt_common::cached_fs::read_to_string;
use anyhow::Context; use anyhow::Context;
use revive_dt_config::Arguments; use revive_dt_config::Arguments;
use tracing::info;
use crate::Node; use crate::Node;
@@ -62,7 +64,16 @@ where
fn spawn_node<T: Node + Send>(args: &Arguments, genesis: String) -> anyhow::Result<T> { fn spawn_node<T: Node + Send>(args: &Arguments, genesis: String) -> anyhow::Result<T> {
let mut node = T::new(args); let mut node = T::new(args);
tracing::info!("starting node: {}", node.connection_string()); info!(
id = node.id(),
connection_string = node.connection_string(),
"Spawning node"
);
node.spawn(genesis)?; node.spawn(genesis)?;
info!(
id = node.id(),
connection_string = node.connection_string(),
"Spawned node"
);
Ok(node) Ok(node)
} }
+11 -1
View File
@@ -8,11 +8,21 @@ repository.workspace = true
rust-version.workspace = true rust-version.workspace = true
[dependencies] [dependencies]
revive-dt-common = { workspace = true }
revive-dt-config = { workspace = true } revive-dt-config = { workspace = true }
revive-dt-format = { workspace = true } revive-dt-format = { workspace = true }
revive-dt-compiler = { workspace = true } revive-dt-compiler = { workspace = true }
alloy-primitives = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
tracing = { workspace = true } paste = { workspace = true }
indexmap = { workspace = true, features = ["serde"] }
semver = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
serde_with = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
[lints]
workspace = true
+550
View File
@@ -0,0 +1,550 @@
//! Implementation of the report aggregator task which consumes the events sent by the various
//! reporters and combines them into a single unified report.
use std::{
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
fs::OpenOptions,
path::PathBuf,
time::{SystemTime, UNIX_EPOCH},
};
use alloy_primitives::Address;
use anyhow::Result;
use indexmap::IndexMap;
use revive_dt_compiler::{CompilerInput, CompilerOutput, Mode};
use revive_dt_config::{Arguments, TestingPlatform};
use revive_dt_format::{case::CaseIdx, corpus::Corpus, metadata::ContractInstance};
use semver::Version;
use serde::Serialize;
use serde_with::{DisplayFromStr, serde_as};
use tokio::sync::{
broadcast::{Sender, channel},
mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel},
};
use tracing::debug;
use crate::*;
pub struct ReportAggregator {
/* Internal Report State */
report: Report,
remaining_cases: HashMap<MetadataFilePath, HashMap<Mode, HashSet<CaseIdx>>>,
/* Channels */
runner_tx: Option<UnboundedSender<RunnerEvent>>,
runner_rx: UnboundedReceiver<RunnerEvent>,
listener_tx: Sender<ReporterEvent>,
}
impl ReportAggregator {
pub fn new(config: Arguments) -> Self {
let (runner_tx, runner_rx) = unbounded_channel::<RunnerEvent>();
let (listener_tx, _) = channel::<ReporterEvent>(1024);
Self {
report: Report::new(config),
remaining_cases: Default::default(),
runner_tx: Some(runner_tx),
runner_rx,
listener_tx,
}
}
pub fn into_task(mut self) -> (Reporter, impl Future<Output = Result<()>>) {
let reporter = self
.runner_tx
.take()
.map(Into::into)
.expect("Can't fail since this can only be called once");
(reporter, async move { self.aggregate().await })
}
async fn aggregate(mut self) -> Result<()> {
debug!("Starting to aggregate report");
while let Some(event) = self.runner_rx.recv().await {
debug!(?event, "Received Event");
match event {
RunnerEvent::SubscribeToEvents(event) => {
self.handle_subscribe_to_events_event(*event);
}
RunnerEvent::CorpusFileDiscovery(event) => {
self.handle_corpus_file_discovered_event(*event)
}
RunnerEvent::MetadataFileDiscovery(event) => {
self.handle_metadata_file_discovery_event(*event);
}
RunnerEvent::TestCaseDiscovery(event) => {
self.handle_test_case_discovery(*event);
}
RunnerEvent::TestSucceeded(event) => {
self.handle_test_succeeded_event(*event);
}
RunnerEvent::TestFailed(event) => {
self.handle_test_failed_event(*event);
}
RunnerEvent::TestIgnored(event) => {
self.handle_test_ignored_event(*event);
}
RunnerEvent::LeaderNodeAssigned(event) => {
self.handle_leader_node_assigned_event(*event);
}
RunnerEvent::FollowerNodeAssigned(event) => {
self.handle_follower_node_assigned_event(*event);
}
RunnerEvent::PreLinkContractsCompilationSucceeded(event) => {
self.handle_pre_link_contracts_compilation_succeeded_event(*event)
}
RunnerEvent::PostLinkContractsCompilationSucceeded(event) => {
self.handle_post_link_contracts_compilation_succeeded_event(*event)
}
RunnerEvent::PreLinkContractsCompilationFailed(event) => {
self.handle_pre_link_contracts_compilation_failed_event(*event)
}
RunnerEvent::PostLinkContractsCompilationFailed(event) => {
self.handle_post_link_contracts_compilation_failed_event(*event)
}
RunnerEvent::LibrariesDeployed(event) => {
self.handle_libraries_deployed_event(*event);
}
RunnerEvent::ContractDeployed(event) => {
self.handle_contract_deployed_event(*event);
}
}
}
debug!("Report aggregation completed");
let file_name = {
let current_timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
let mut file_name = current_timestamp.to_string();
file_name.push_str(".json");
file_name
};
let file_path = self.report.config.directory().join(file_name);
let file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.read(false)
.open(file_path)?;
serde_json::to_writer_pretty(file, &self.report)?;
Ok(())
}
fn handle_subscribe_to_events_event(&self, event: SubscribeToEventsEvent) {
let _ = event.tx.send(self.listener_tx.subscribe());
}
fn handle_corpus_file_discovered_event(&mut self, event: CorpusFileDiscoveryEvent) {
self.report.corpora.push(event.corpus);
}
fn handle_metadata_file_discovery_event(&mut self, event: MetadataFileDiscoveryEvent) {
self.report.metadata_files.insert(event.path.clone());
}
fn handle_test_case_discovery(&mut self, event: TestCaseDiscoveryEvent) {
self.remaining_cases
.entry(event.test_specifier.metadata_file_path.clone().into())
.or_default()
.entry(event.test_specifier.solc_mode.clone())
.or_default()
.insert(event.test_specifier.case_idx);
}
fn handle_test_succeeded_event(&mut self, event: TestSucceededEvent) {
// Remove this from the set of cases we're tracking since it has completed.
self.remaining_cases
.entry(event.test_specifier.metadata_file_path.clone().into())
.or_default()
.entry(event.test_specifier.solc_mode.clone())
.or_default()
.remove(&event.test_specifier.case_idx);
// Add information on the fact that the case was ignored to the report.
let test_case_report = self.test_case_report(&event.test_specifier);
test_case_report.status = Some(TestCaseStatus::Succeeded {
steps_executed: event.steps_executed,
});
self.handle_post_test_case_status_update(&event.test_specifier);
}
fn handle_test_failed_event(&mut self, event: TestFailedEvent) {
// Remove this from the set of cases we're tracking since it has completed.
self.remaining_cases
.entry(event.test_specifier.metadata_file_path.clone().into())
.or_default()
.entry(event.test_specifier.solc_mode.clone())
.or_default()
.remove(&event.test_specifier.case_idx);
// Add information on the fact that the case was ignored to the report.
let test_case_report = self.test_case_report(&event.test_specifier);
test_case_report.status = Some(TestCaseStatus::Failed {
reason: event.reason,
});
self.handle_post_test_case_status_update(&event.test_specifier);
}
fn handle_test_ignored_event(&mut self, event: TestIgnoredEvent) {
// Remove this from the set of cases we're tracking since it has completed.
self.remaining_cases
.entry(event.test_specifier.metadata_file_path.clone().into())
.or_default()
.entry(event.test_specifier.solc_mode.clone())
.or_default()
.remove(&event.test_specifier.case_idx);
// Add information on the fact that the case was ignored to the report.
let test_case_report = self.test_case_report(&event.test_specifier);
test_case_report.status = Some(TestCaseStatus::Ignored {
reason: event.reason,
additional_fields: event.additional_fields,
});
self.handle_post_test_case_status_update(&event.test_specifier);
}
fn handle_post_test_case_status_update(&mut self, specifier: &TestSpecifier) {
let remaining_cases = self
.remaining_cases
.entry(specifier.metadata_file_path.clone().into())
.or_default()
.entry(specifier.solc_mode.clone())
.or_default();
if !remaining_cases.is_empty() {
return;
}
let case_status = self
.report
.test_case_information
.entry(specifier.metadata_file_path.clone().into())
.or_default()
.entry(specifier.solc_mode.clone())
.or_default()
.iter()
.map(|(case_idx, case_report)| {
(
*case_idx,
case_report.status.clone().expect("Can't be uninitialized"),
)
})
.collect::<BTreeMap<_, _>>();
let event = ReporterEvent::MetadataFileSolcModeCombinationExecutionCompleted {
metadata_file_path: specifier.metadata_file_path.clone().into(),
mode: specifier.solc_mode.clone(),
case_status,
};
// According to the documentation on send, the sending fails if there are no more receiver
// handles. Therefore, this isn't an error that we want to bubble up or anything. If we fail
// to send then we ignore the error.
let _ = self.listener_tx.send(event);
}
fn handle_leader_node_assigned_event(&mut self, event: LeaderNodeAssignedEvent) {
let execution_information = self.execution_information(&ExecutionSpecifier {
test_specifier: event.test_specifier,
node_id: event.id,
node_designation: NodeDesignation::Leader,
});
execution_information.node = Some(TestCaseNodeInformation {
id: event.id,
platform: event.platform,
connection_string: event.connection_string,
});
}
fn handle_follower_node_assigned_event(&mut self, event: FollowerNodeAssignedEvent) {
let execution_information = self.execution_information(&ExecutionSpecifier {
test_specifier: event.test_specifier,
node_id: event.id,
node_designation: NodeDesignation::Follower,
});
execution_information.node = Some(TestCaseNodeInformation {
id: event.id,
platform: event.platform,
connection_string: event.connection_string,
});
}
fn handle_pre_link_contracts_compilation_succeeded_event(
&mut self,
event: PreLinkContractsCompilationSucceededEvent,
) {
let include_input = self.report.config.report_include_compiler_input;
let include_output = self.report.config.report_include_compiler_output;
let execution_information = self.execution_information(&event.execution_specifier);
let compiler_input = if include_input {
event.compiler_input
} else {
None
};
let compiler_output = if include_output {
Some(event.compiler_output)
} else {
None
};
execution_information.pre_link_compilation_status = Some(CompilationStatus::Success {
is_cached: event.is_cached,
compiler_version: event.compiler_version,
compiler_path: event.compiler_path,
compiler_input,
compiler_output,
});
}
fn handle_post_link_contracts_compilation_succeeded_event(
&mut self,
event: PostLinkContractsCompilationSucceededEvent,
) {
let include_input = self.report.config.report_include_compiler_input;
let include_output = self.report.config.report_include_compiler_output;
let execution_information = self.execution_information(&event.execution_specifier);
let compiler_input = if include_input {
event.compiler_input
} else {
None
};
let compiler_output = if include_output {
Some(event.compiler_output)
} else {
None
};
execution_information.post_link_compilation_status = Some(CompilationStatus::Success {
is_cached: event.is_cached,
compiler_version: event.compiler_version,
compiler_path: event.compiler_path,
compiler_input,
compiler_output,
});
}
fn handle_pre_link_contracts_compilation_failed_event(
&mut self,
event: PreLinkContractsCompilationFailedEvent,
) {
let include_input = self.report.config.report_include_compiler_input;
let execution_information = self.execution_information(&event.execution_specifier);
let compiler_input = if include_input {
event.compiler_input
} else {
None
};
execution_information.pre_link_compilation_status = Some(CompilationStatus::Failure {
reason: event.reason,
compiler_version: event.compiler_version,
compiler_path: event.compiler_path,
compiler_input,
});
}
fn handle_post_link_contracts_compilation_failed_event(
&mut self,
event: PostLinkContractsCompilationFailedEvent,
) {
let include_input = self.report.config.report_include_compiler_input;
let execution_information = self.execution_information(&event.execution_specifier);
let compiler_input = if include_input {
event.compiler_input
} else {
None
};
execution_information.post_link_compilation_status = Some(CompilationStatus::Failure {
reason: event.reason,
compiler_version: event.compiler_version,
compiler_path: event.compiler_path,
compiler_input,
});
}
fn handle_libraries_deployed_event(&mut self, event: LibrariesDeployedEvent) {
self.execution_information(&event.execution_specifier)
.deployed_libraries = Some(event.libraries);
}
fn handle_contract_deployed_event(&mut self, event: ContractDeployedEvent) {
self.execution_information(&event.execution_specifier)
.deployed_contracts
.get_or_insert_default()
.insert(event.contract_instance, event.address);
}
fn test_case_report(&mut self, specifier: &TestSpecifier) -> &mut TestCaseReport {
self.report
.test_case_information
.entry(specifier.metadata_file_path.clone().into())
.or_default()
.entry(specifier.solc_mode.clone())
.or_default()
.entry(specifier.case_idx)
.or_default()
}
fn execution_information(
&mut self,
specifier: &ExecutionSpecifier,
) -> &mut ExecutionInformation {
let test_case_report = self.test_case_report(&specifier.test_specifier);
match specifier.node_designation {
NodeDesignation::Leader => test_case_report
.leader_execution_information
.get_or_insert_default(),
NodeDesignation::Follower => test_case_report
.follower_execution_information
.get_or_insert_default(),
}
}
}
#[serde_as]
#[derive(Clone, Debug, Serialize)]
pub struct Report {
/// The configuration that the tool was started up with.
pub config: Arguments,
/// The platform of the leader chain.
pub leader_platform: TestingPlatform,
/// The platform of the follower chain.
pub follower_platform: TestingPlatform,
/// The list of corpus files that the tool found.
pub corpora: Vec<Corpus>,
/// The list of metadata files that were found by the tool.
pub metadata_files: BTreeSet<MetadataFilePath>,
/// Information relating to each test case.
#[serde_as(as = "BTreeMap<_, HashMap<DisplayFromStr, BTreeMap<DisplayFromStr, _>>>")]
pub test_case_information:
BTreeMap<MetadataFilePath, HashMap<Mode, BTreeMap<CaseIdx, TestCaseReport>>>,
}
impl Report {
pub fn new(config: Arguments) -> Self {
Self {
leader_platform: config.leader,
follower_platform: config.follower,
config,
corpora: Default::default(),
metadata_files: Default::default(),
test_case_information: Default::default(),
}
}
}
#[derive(Clone, Debug, Serialize, Default)]
pub struct TestCaseReport {
/// Information on the status of the test case and whether it succeeded, failed, or was ignored.
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<TestCaseStatus>,
/// Information related to the execution on the leader.
#[serde(skip_serializing_if = "Option::is_none")]
pub leader_execution_information: Option<ExecutionInformation>,
/// Information related to the execution on the follower.
#[serde(skip_serializing_if = "Option::is_none")]
pub follower_execution_information: Option<ExecutionInformation>,
}
/// Information related to the status of the test. Could be that the test succeeded, failed, or that
/// it was ignored.
#[derive(Clone, Debug, Serialize)]
#[serde(tag = "status")]
pub enum TestCaseStatus {
/// The test case succeeded.
Succeeded {
/// The number of steps of the case that were executed.
steps_executed: usize,
},
/// The test case failed.
Failed {
/// The reason for the failure of the test case.
reason: String,
},
/// The test case was ignored. This variant carries information related to why it was ignored.
Ignored {
/// The reason behind the test case being ignored.
reason: String,
/// Additional fields that describe more information on why the test case is ignored.
#[serde(flatten)]
additional_fields: IndexMap<String, serde_json::Value>,
},
}
/// Information related to the leader or follower node that's being used to execute the step.
#[derive(Clone, Debug, Serialize)]
pub struct TestCaseNodeInformation {
/// The ID of the node that this case is being executed on.
pub id: usize,
/// The platform of the node.
pub platform: TestingPlatform,
/// The connection string of the node.
pub connection_string: String,
}
/// Execution information tied to the leader or the follower.
#[derive(Clone, Debug, Default, Serialize)]
pub struct ExecutionInformation {
/// Information related to the node assigned to this test case.
#[serde(skip_serializing_if = "Option::is_none")]
pub node: Option<TestCaseNodeInformation>,
/// Information on the pre-link compiled contracts.
#[serde(skip_serializing_if = "Option::is_none")]
pub pre_link_compilation_status: Option<CompilationStatus>,
/// Information on the post-link compiled contracts.
#[serde(skip_serializing_if = "Option::is_none")]
pub post_link_compilation_status: Option<CompilationStatus>,
/// Information on the deployed libraries.
#[serde(skip_serializing_if = "Option::is_none")]
pub deployed_libraries: Option<BTreeMap<ContractInstance, Address>>,
/// Information on the deployed contracts.
#[serde(skip_serializing_if = "Option::is_none")]
pub deployed_contracts: Option<BTreeMap<ContractInstance, Address>>,
}
/// Information related to compilation
#[derive(Clone, Debug, Serialize)]
#[serde(tag = "status")]
pub enum CompilationStatus {
/// The compilation was successful.
Success {
/// A flag with information on whether the compilation artifacts were cached or not.
is_cached: bool,
/// The version of the compiler used to compile the contracts.
compiler_version: Version,
/// The path of the compiler used to compile the contracts.
compiler_path: PathBuf,
/// The input provided to the compiler to compile the contracts. This is only included if
/// the appropriate flag is set in the CLI configuration and if the contracts were not
/// cached and the compiler was invoked.
#[serde(skip_serializing_if = "Option::is_none")]
compiler_input: Option<CompilerInput>,
/// The output of the compiler. This is only included if the appropriate flag is set in the
/// CLI configurations.
#[serde(skip_serializing_if = "Option::is_none")]
compiler_output: Option<CompilerOutput>,
},
/// The compilation failed.
Failure {
/// The failure reason.
reason: String,
/// The version of the compiler used to compile the contracts.
#[serde(skip_serializing_if = "Option::is_none")]
compiler_version: Option<Version>,
/// The path of the compiler used to compile the contracts.
#[serde(skip_serializing_if = "Option::is_none")]
compiler_path: Option<PathBuf>,
/// The input provided to the compiler to compile the contracts. This is only included if
/// the appropriate flag is set in the CLI configuration and if the contracts were not
/// cached and the compiler was invoked.
#[serde(skip_serializing_if = "Option::is_none")]
compiler_input: Option<CompilerInput>,
},
}
-81
View File
@@ -1,81 +0,0 @@
//! The report analyzer enriches the raw report data.
use revive_dt_compiler::CompilerOutput;
use serde::{Deserialize, Serialize};
use crate::reporter::CompilationTask;
/// Provides insights into how well the compilers perform.
#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, PartialOrd)]
pub struct CompilerStatistics {
/// The sum of contracts observed.
pub n_contracts: usize,
/// The mean size of compiled contracts.
pub mean_code_size: usize,
/// The mean size of the optimized YUL IR.
pub mean_yul_size: usize,
/// Is a proxy because the YUL also contains a lot of comments.
pub yul_to_bytecode_size_ratio: f32,
}
impl CompilerStatistics {
/// Cumulatively update the statistics with the next compiler task.
pub fn sample(&mut self, compilation_task: &CompilationTask) {
let Some(CompilerOutput { contracts }) = &compilation_task.json_output else {
return;
};
for (_solidity, contracts) in contracts.iter() {
for (_name, (bytecode, _)) in contracts.iter() {
// The EVM bytecode can be unlinked and thus is not necessarily a decodable hex
// string; for our statistics this is a good enough approximation.
let bytecode_size = bytecode.len() / 2;
// TODO: for the time being we set the yul_size to be zero. We need to change this
// when we overhaul the reporting.
self.update_sizes(bytecode_size, 0);
}
}
}
/// Updates the size statistics cumulatively.
fn update_sizes(&mut self, bytecode_size: usize, yul_size: usize) {
let n_previous = self.n_contracts;
let n_current = self.n_contracts + 1;
self.n_contracts = n_current;
self.mean_code_size = (n_previous * self.mean_code_size + bytecode_size) / n_current;
self.mean_yul_size = (n_previous * self.mean_yul_size + yul_size) / n_current;
if self.mean_code_size > 0 {
self.yul_to_bytecode_size_ratio =
self.mean_yul_size as f32 / self.mean_code_size as f32;
}
}
}
#[cfg(test)]
mod tests {
use super::CompilerStatistics;
#[test]
fn compiler_statistics() {
let mut received = CompilerStatistics::default();
received.update_sizes(0, 0);
received.update_sizes(3, 37);
received.update_sizes(123, 456);
let mean_code_size = 41; // rounding error from integer truncation
let mean_yul_size = 164;
let expected = CompilerStatistics {
n_contracts: 3,
mean_code_size,
mean_yul_size,
yul_to_bytecode_size_ratio: mean_yul_size as f32 / mean_code_size as f32,
};
assert_eq!(received, expected);
}
}
+43
View File
@@ -0,0 +1,43 @@
//! Common types and functions used throughout the crate.
use std::{path::PathBuf, sync::Arc};
use revive_dt_common::define_wrapper_type;
use revive_dt_compiler::Mode;
use revive_dt_format::{case::CaseIdx, input::StepIdx};
use serde::{Deserialize, Serialize};
define_wrapper_type!(
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct MetadataFilePath(PathBuf);
);
/// An absolute specifier for a test.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct TestSpecifier {
pub solc_mode: Mode,
pub metadata_file_path: PathBuf,
pub case_idx: CaseIdx,
}
/// An absolute path for a test that also includes information about the node that it's assigned to
/// and whether it's the leader or follower.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ExecutionSpecifier {
pub test_specifier: Arc<TestSpecifier>,
pub node_id: usize,
pub node_designation: NodeDesignation,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum NodeDesignation {
Leader,
Follower,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct StepExecutionSpecifier {
pub execution_specifier: Arc<ExecutionSpecifier>,
pub step_idx: StepIdx,
}
+10 -3
View File
@@ -1,4 +1,11 @@
//! The revive differential tests reporting facility. //! This crate implements the reporting infrastructure for the differential testing tool.
pub mod analyzer; mod aggregator;
pub mod reporter; mod common;
mod reporter_event;
mod runner_event;
pub use aggregator::*;
pub use common::*;
pub use reporter_event::*;
pub use runner_event::*;
-235
View File
@@ -1,235 +0,0 @@
//! The reporter is the central place observing test execution by collecting data.
//!
//! The data collected gives useful insights into the outcome of the test run
//! and helps identifying and reproducing failing cases.
use std::{
collections::HashMap,
fs::{self, File, create_dir_all},
path::PathBuf,
sync::{Mutex, OnceLock},
time::{SystemTime, UNIX_EPOCH},
};
use anyhow::Context;
use revive_dt_compiler::{CompilerInput, CompilerOutput};
use serde::{Deserialize, Serialize};
use revive_dt_config::{Arguments, TestingPlatform};
use revive_dt_format::{corpus::Corpus, mode::SolcMode};
use crate::analyzer::CompilerStatistics;
pub(crate) static REPORTER: OnceLock<Mutex<Report>> = OnceLock::new();
/// The `Report` datastructure stores all relevant inforamtion required for generating reports.
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Report {
/// The configuration used during the test.
pub config: Arguments,
/// The observed test corpora.
pub corpora: Vec<Corpus>,
/// The observed test definitions.
pub metadata_files: Vec<PathBuf>,
/// The observed compilation results.
pub compiler_results: HashMap<TestingPlatform, Vec<CompilationResult>>,
/// The observed compilation statistics.
pub compiler_statistics: HashMap<TestingPlatform, CompilerStatistics>,
/// The file name this is serialized to.
#[serde(skip)]
directory: PathBuf,
}
/// Contains a compiled contract.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CompilationTask {
/// The observed compiler input.
pub json_input: CompilerInput,
/// The observed compiler output.
pub json_output: Option<CompilerOutput>,
/// The observed compiler mode.
pub mode: SolcMode,
/// The observed compiler version.
pub compiler_version: String,
/// The observed error, if any.
pub error: Option<String>,
}
/// Represents a report about a compilation task.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CompilationResult {
/// The observed compilation task.
pub compilation_task: CompilationTask,
/// The linked span.
pub span: Span,
}
/// The [Span] struct indicates the context of what is being reported.
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct Span {
/// The corpus index this belongs to.
corpus: usize,
/// The metadata file this belongs to.
metadata_file: usize,
/// The index of the case definition this belongs to.
case: usize,
/// The index of the case input this belongs to.
input: usize,
}
impl Report {
/// The file name where this report will be written to.
pub const FILE_NAME: &str = "report.json";
/// The [Span] is expected to initialize the reporter by providing the config.
const INITIALIZED_VIA_SPAN: &str = "requires a Span which initializes the reporter";
/// Create a new [Report].
fn new(config: Arguments) -> anyhow::Result<Self> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis();
let directory = config.directory().join("report").join(format!("{now}"));
if !directory.exists() {
create_dir_all(&directory)?;
}
Ok(Self {
config,
directory,
..Default::default()
})
}
/// Add a compilation task to the report.
pub fn compilation(span: Span, platform: TestingPlatform, compilation_task: CompilationTask) {
let mut report = REPORTER
.get()
.expect(Report::INITIALIZED_VIA_SPAN)
.lock()
.unwrap();
report
.compiler_statistics
.entry(platform)
.or_default()
.sample(&compilation_task);
report
.compiler_results
.entry(platform)
.or_default()
.push(CompilationResult {
compilation_task,
span,
});
}
/// Write the report to disk.
pub fn save() -> anyhow::Result<()> {
let Some(reporter) = REPORTER.get() else {
return Ok(());
};
let report = reporter.lock().unwrap();
if let Err(error) = report.write_to_file() {
anyhow::bail!("can not write report: {error}");
}
if report.config.extract_problems {
if let Err(error) = report.save_compiler_problems() {
anyhow::bail!("can not write compiler problems: {error}");
}
}
Ok(())
}
/// Write compiler problems to disk for later debugging.
pub fn save_compiler_problems(&self) -> anyhow::Result<()> {
for (platform, results) in self.compiler_results.iter() {
for result in results {
// ignore if there were no errors
if result.compilation_task.error.is_none() {
continue;
}
let path = &self.metadata_files[result.span.metadata_file]
.parent()
.unwrap()
.join(format!("{platform}_errors"));
if !path.exists() {
create_dir_all(path)?;
}
if let Some(error) = result.compilation_task.error.as_ref() {
fs::write(path.join("compiler_error.txt"), error)?;
}
if let Some(errors) = result.compilation_task.json_output.as_ref() {
let file = File::create(path.join("compiler_output.txt"))?;
serde_json::to_writer_pretty(file, &errors)?;
}
}
}
Ok(())
}
fn write_to_file(&self) -> anyhow::Result<()> {
let path = self.directory.join(Self::FILE_NAME);
let file = File::create(&path).context(path.display().to_string())?;
serde_json::to_writer_pretty(file, &self)?;
tracing::info!("report written to: {}", path.display());
Ok(())
}
}
impl Span {
/// Create a new [Span] with case and input index at 0.
///
/// Initializes the reporting facility on the first call.
pub fn new(corpus: Corpus, config: Arguments) -> anyhow::Result<Self> {
let report = Mutex::new(Report::new(config)?);
let mut reporter = REPORTER.get_or_init(|| report).lock().unwrap();
reporter.corpora.push(corpus);
Ok(Self {
corpus: reporter.corpora.len() - 1,
metadata_file: 0,
case: 0,
input: 0,
})
}
/// Advance to the next metadata file: Resets the case input index to 0.
pub fn next_metadata(&mut self, metadata_file: PathBuf) {
let mut reporter = REPORTER
.get()
.expect(Report::INITIALIZED_VIA_SPAN)
.lock()
.unwrap();
reporter.metadata_files.push(metadata_file);
self.metadata_file = reporter.metadata_files.len() - 1;
self.case = 0;
self.input = 0;
}
/// Advance to the next case: Increas the case index by one and resets the input index to 0.
pub fn next_case(&mut self) {
self.case += 1;
self.input = 0;
}
/// Advance to the next input.
pub fn next_input(&mut self) {
self.input += 1;
}
}
+22
View File
@@ -0,0 +1,22 @@
//! A reporter event sent by the report aggregator to the various listeners.
use std::collections::BTreeMap;
use revive_dt_compiler::Mode;
use revive_dt_format::case::CaseIdx;
use crate::{MetadataFilePath, TestCaseStatus};
#[derive(Clone, Debug)]
pub enum ReporterEvent {
/// An event sent by the reporter once an entire metadata file and solc mode combination has
/// finished execution.
MetadataFileSolcModeCombinationExecutionCompleted {
/// The path of the metadata file.
metadata_file_path: MetadataFilePath,
/// The Solc mode that this metadata file was executed in.
mode: Mode,
/// The status of each one of the cases.
case_status: BTreeMap<CaseIdx, TestCaseStatus>,
},
}
+640
View File
@@ -0,0 +1,640 @@
//! The types associated with the events sent by the runner to the reporter.
#![allow(dead_code)]
use std::{collections::BTreeMap, path::PathBuf, sync::Arc};
use alloy_primitives::Address;
use indexmap::IndexMap;
use revive_dt_compiler::{CompilerInput, CompilerOutput};
use revive_dt_config::TestingPlatform;
use revive_dt_format::metadata::Metadata;
use revive_dt_format::{corpus::Corpus, metadata::ContractInstance};
use semver::Version;
use tokio::sync::{broadcast, oneshot};
use crate::{ExecutionSpecifier, ReporterEvent, TestSpecifier, common::MetadataFilePath};
macro_rules! __report_gen_emit_test_specific {
(
$ident:ident,
$variant_ident:ident,
$skip_field:ident;
$( $bname:ident : $bty:ty, )*
;
$( $aname:ident : $aty:ty, )*
) => {
paste::paste! {
pub fn [< report_ $variant_ident:snake _event >](
&self
$(, $bname: impl Into<$bty> )*
$(, $aname: impl Into<$aty> )*
) -> anyhow::Result<()> {
self.report([< $variant_ident Event >] {
$skip_field: self.test_specifier.clone()
$(, $bname: $bname.into() )*
$(, $aname: $aname.into() )*
})
}
}
};
}
macro_rules! __report_gen_emit_test_specific_by_parse {
(
$ident:ident,
$variant_ident:ident,
$skip_field:ident;
$( $bname:ident : $bty:ty, )* ; $( $aname:ident : $aty:ty, )*
) => {
__report_gen_emit_test_specific!(
$ident, $variant_ident, $skip_field;
$( $bname : $bty, )* ; $( $aname : $aty, )*
);
};
}
macro_rules! __report_gen_scan_before {
(
$ident:ident, $variant_ident:ident;
$( $before:ident : $bty:ty, )*
;
test_specifier : $skip_ty:ty,
$( $after:ident : $aty:ty, )*
;
) => {
__report_gen_emit_test_specific_by_parse!(
$ident, $variant_ident, test_specifier;
$( $before : $bty, )* ; $( $after : $aty, )*
);
};
(
$ident:ident, $variant_ident:ident;
$( $before:ident : $bty:ty, )*
;
$name:ident : $ty:ty, $( $after:ident : $aty:ty, )*
;
) => {
__report_gen_scan_before!(
$ident, $variant_ident;
$( $before : $bty, )* $name : $ty,
;
$( $after : $aty, )*
;
);
};
(
$ident:ident, $variant_ident:ident;
$( $before:ident : $bty:ty, )*
;
;
) => {};
}
macro_rules! __report_gen_for_variant {
(
$ident:ident,
$variant_ident:ident;
) => {};
(
$ident:ident,
$variant_ident:ident;
$( $field_ident:ident : $field_ty:ty ),+ $(,)?
) => {
__report_gen_scan_before!(
$ident, $variant_ident;
;
$( $field_ident : $field_ty, )*
;
);
};
}
macro_rules! __report_gen_emit_execution_specific {
(
$ident:ident,
$variant_ident:ident,
$skip_field:ident;
$( $bname:ident : $bty:ty, )*
;
$( $aname:ident : $aty:ty, )*
) => {
paste::paste! {
pub fn [< report_ $variant_ident:snake _event >](
&self
$(, $bname: impl Into<$bty> )*
$(, $aname: impl Into<$aty> )*
) -> anyhow::Result<()> {
self.report([< $variant_ident Event >] {
$skip_field: self.execution_specifier.clone()
$(, $bname: $bname.into() )*
$(, $aname: $aname.into() )*
})
}
}
};
}
macro_rules! __report_gen_emit_execution_specific_by_parse {
(
$ident:ident,
$variant_ident:ident,
$skip_field:ident;
$( $bname:ident : $bty:ty, )* ; $( $aname:ident : $aty:ty, )*
) => {
__report_gen_emit_execution_specific!(
$ident, $variant_ident, $skip_field;
$( $bname : $bty, )* ; $( $aname : $aty, )*
);
};
}
macro_rules! __report_gen_scan_before_exec {
(
$ident:ident, $variant_ident:ident;
$( $before:ident : $bty:ty, )*
;
execution_specifier : $skip_ty:ty,
$( $after:ident : $aty:ty, )*
;
) => {
__report_gen_emit_execution_specific_by_parse!(
$ident, $variant_ident, execution_specifier;
$( $before : $bty, )* ; $( $after : $aty, )*
);
};
(
$ident:ident, $variant_ident:ident;
$( $before:ident : $bty:ty, )*
;
$name:ident : $ty:ty, $( $after:ident : $aty:ty, )*
;
) => {
__report_gen_scan_before_exec!(
$ident, $variant_ident;
$( $before : $bty, )* $name : $ty,
;
$( $after : $aty, )*
;
);
};
(
$ident:ident, $variant_ident:ident;
$( $before:ident : $bty:ty, )*
;
;
) => {};
}
macro_rules! __report_gen_for_variant_exec {
(
$ident:ident,
$variant_ident:ident;
) => {};
(
$ident:ident,
$variant_ident:ident;
$( $field_ident:ident : $field_ty:ty ),+ $(,)?
) => {
__report_gen_scan_before_exec!(
$ident, $variant_ident;
;
$( $field_ident : $field_ty, )*
;
);
};
}
macro_rules! __report_gen_emit_step_execution_specific {
(
$ident:ident,
$variant_ident:ident,
$skip_field:ident;
$( $bname:ident : $bty:ty, )*
;
$( $aname:ident : $aty:ty, )*
) => {
paste::paste! {
pub fn [< report_ $variant_ident:snake _event >](
&self
$(, $bname: impl Into<$bty> )*
$(, $aname: impl Into<$aty> )*
) -> anyhow::Result<()> {
self.report([< $variant_ident Event >] {
$skip_field: self.step_specifier.clone()
$(, $bname: $bname.into() )*
$(, $aname: $aname.into() )*
})
}
}
};
}
macro_rules! __report_gen_emit_step_execution_specific_by_parse {
(
$ident:ident,
$variant_ident:ident,
$skip_field:ident;
$( $bname:ident : $bty:ty, )* ; $( $aname:ident : $aty:ty, )*
) => {
__report_gen_emit_step_execution_specific!(
$ident, $variant_ident, $skip_field;
$( $bname : $bty, )* ; $( $aname : $aty, )*
);
};
}
macro_rules! __report_gen_scan_before_step {
(
$ident:ident, $variant_ident:ident;
$( $before:ident : $bty:ty, )*
;
step_specifier : $skip_ty:ty,
$( $after:ident : $aty:ty, )*
;
) => {
__report_gen_emit_step_execution_specific_by_parse!(
$ident, $variant_ident, step_specifier;
$( $before : $bty, )* ; $( $after : $aty, )*
);
};
(
$ident:ident, $variant_ident:ident;
$( $before:ident : $bty:ty, )*
;
$name:ident : $ty:ty, $( $after:ident : $aty:ty, )*
;
) => {
__report_gen_scan_before_step!(
$ident, $variant_ident;
$( $before : $bty, )* $name : $ty,
;
$( $after : $aty, )*
;
);
};
(
$ident:ident, $variant_ident:ident;
$( $before:ident : $bty:ty, )*
;
;
) => {};
}
macro_rules! __report_gen_for_variant_step {
(
$ident:ident,
$variant_ident:ident;
) => {};
(
$ident:ident,
$variant_ident:ident;
$( $field_ident:ident : $field_ty:ty ),+ $(,)?
) => {
__report_gen_scan_before_step!(
$ident, $variant_ident;
;
$( $field_ident : $field_ty, )*
;
);
};
}
/// Defines the runner-event which is sent from the test runners to the report aggregator.
///
/// This macro defines a number of things related to the reporting infrastructure and the interface
/// used. First of all, it defines the enum of all of the possible events that the runners can send
/// to the aggregator. For each one of the variants it defines a separate struct for it to allow the
/// variant field in the enum to be put in a [`Box`].
///
/// In addition to the above, it defines [`From`] implementations for the various event types for
/// the [`RunnerEvent`] enum essentially allowing for events such as [`CorpusFileDiscoveryEvent`] to
/// be converted into a [`RunnerEvent`].
///
/// In addition to the above, it also defines the [`RunnerEventReporter`] which is a wrapper around
/// an [`UnboundedSender`] allowing for events to be sent to the report aggregator.
///
/// With the above description, we can see that this macro defines almost all of the interface of
/// the reporting infrastructure, from the enum itself, to its associated types, and also to the
/// reporter that's used to report events to the aggregator.
///
/// [`UnboundedSender`]: tokio::sync::mpsc::UnboundedSender
macro_rules! define_event {
(
$(#[$enum_meta: meta])*
$vis: vis enum $ident: ident {
$(
$(#[$variant_meta: meta])*
$variant_ident: ident {
$(
$(#[$field_meta: meta])*
$field_ident: ident: $field_ty: ty
),* $(,)?
}
),* $(,)?
}
) => {
paste::paste! {
$(#[$enum_meta])*
#[derive(Debug)]
$vis enum $ident {
$(
$(#[$variant_meta])*
$variant_ident(Box<[<$variant_ident Event>]>)
),*
}
$(
#[derive(Debug)]
$(#[$variant_meta])*
$vis struct [<$variant_ident Event>] {
$(
$(#[$field_meta])*
$vis $field_ident: $field_ty
),*
}
)*
$(
impl From<[<$variant_ident Event>]> for $ident {
fn from(value: [<$variant_ident Event>]) -> Self {
Self::$variant_ident(Box::new(value))
}
}
)*
/// Provides a way to report events to the aggregator.
///
/// Under the hood, this is a wrapper around an [`UnboundedSender`] which abstracts away
/// the fact that channels are used and that implements high-level methods for reporting
/// various events to the aggregator.
#[derive(Clone, Debug)]
pub struct [< $ident Reporter >]($vis tokio::sync::mpsc::UnboundedSender<$ident>);
impl From<tokio::sync::mpsc::UnboundedSender<$ident>> for [< $ident Reporter >] {
fn from(value: tokio::sync::mpsc::UnboundedSender<$ident>) -> Self {
Self(value)
}
}
impl [< $ident Reporter >] {
pub fn test_specific_reporter(
&self,
test_specifier: impl Into<std::sync::Arc<crate::common::TestSpecifier>>
) -> [< $ident TestSpecificReporter >] {
[< $ident TestSpecificReporter >] {
reporter: self.clone(),
test_specifier: test_specifier.into(),
}
}
fn report(&self, event: impl Into<$ident>) -> anyhow::Result<()> {
self.0.send(event.into()).map_err(Into::into)
}
$(
pub fn [< report_ $variant_ident:snake _event >](&self, $($field_ident: impl Into<$field_ty>),*) -> anyhow::Result<()> {
self.report([< $variant_ident Event >] {
$($field_ident: $field_ident.into()),*
})
}
)*
}
/// A reporter that's tied to a specific test case.
#[derive(Clone, Debug)]
pub struct [< $ident TestSpecificReporter >] {
$vis reporter: [< $ident Reporter >],
$vis test_specifier: std::sync::Arc<crate::common::TestSpecifier>,
}
impl [< $ident TestSpecificReporter >] {
pub fn execution_specific_reporter(
&self,
node_id: impl Into<usize>,
node_designation: impl Into<$crate::common::NodeDesignation>
) -> [< $ident ExecutionSpecificReporter >] {
[< $ident ExecutionSpecificReporter >] {
reporter: self.reporter.clone(),
execution_specifier: Arc::new($crate::common::ExecutionSpecifier {
test_specifier: self.test_specifier.clone(),
node_id: node_id.into(),
node_designation: node_designation.into(),
})
}
}
fn report(&self, event: impl Into<$ident>) -> anyhow::Result<()> {
self.reporter.report(event)
}
$(
__report_gen_for_variant! { $ident, $variant_ident; $( $field_ident : $field_ty ),* }
)*
}
/// A reporter that's tied to a specific execution of the test case such as execution on
/// a specific node like the leader or follower.
#[derive(Clone, Debug)]
pub struct [< $ident ExecutionSpecificReporter >] {
$vis reporter: [< $ident Reporter >],
$vis execution_specifier: std::sync::Arc<$crate::common::ExecutionSpecifier>,
}
impl [< $ident ExecutionSpecificReporter >] {
fn report(&self, event: impl Into<$ident>) -> anyhow::Result<()> {
self.reporter.report(event)
}
$(
__report_gen_for_variant_exec! { $ident, $variant_ident; $( $field_ident : $field_ty ),* }
)*
}
/// A reporter that's tied to a specific step execution
#[derive(Clone, Debug)]
pub struct [< $ident StepExecutionSpecificReporter >] {
$vis reporter: [< $ident Reporter >],
$vis step_specifier: std::sync::Arc<$crate::common::StepExecutionSpecifier>,
}
impl [< $ident StepExecutionSpecificReporter >] {
fn report(&self, event: impl Into<$ident>) -> anyhow::Result<()> {
self.reporter.report(event)
}
$(
__report_gen_for_variant_step! { $ident, $variant_ident; $( $field_ident : $field_ty ),* }
)*
}
}
};
}
define_event! {
/// An event type that's sent by the test runners/drivers to the report aggregator.
pub(crate) enum RunnerEvent {
/// An event emitted by the reporter when it wishes to listen to events emitted by the
/// aggregator.
SubscribeToEvents {
/// The channel that the aggregator is to send the receive side of the channel on.
tx: oneshot::Sender<broadcast::Receiver<ReporterEvent>>
},
/// An event emitted by runners when they've discovered a corpus file.
CorpusFileDiscovery {
/// The contents of the corpus file.
corpus: Corpus
},
/// An event emitted by runners when they've discovered a metadata file.
MetadataFileDiscovery {
/// The path of the metadata file discovered.
path: MetadataFilePath,
/// The content of the metadata file.
metadata: Metadata
},
/// An event emitted by the runners when they discover a test case.
TestCaseDiscovery {
/// A specifier for the test that was discovered.
test_specifier: Arc<TestSpecifier>,
},
/// An event emitted by the runners when a test case is ignored.
TestIgnored {
/// A specifier for the test that's been ignored.
test_specifier: Arc<TestSpecifier>,
/// A reason for the test to be ignored.
reason: String,
/// Additional fields that describe more information on why the test was ignored.
additional_fields: IndexMap<String, serde_json::Value>
},
/// An event emitted by the runners when a test case has succeeded.
TestSucceeded {
/// A specifier for the test that succeeded.
test_specifier: Arc<TestSpecifier>,
/// The number of steps of the case that were executed by the driver.
steps_executed: usize,
},
/// An event emitted by the runners when a test case has failed.
TestFailed {
/// A specifier for the test that succeeded.
test_specifier: Arc<TestSpecifier>,
/// A reason for the failure of the test.
reason: String,
},
/// An event emitted when the test case is assigned a leader node.
LeaderNodeAssigned {
/// A specifier for the test that the assignment is for.
test_specifier: Arc<TestSpecifier>,
/// The ID of the node that this case is being executed on.
id: usize,
/// The platform of the node.
platform: TestingPlatform,
/// The connection string of the node.
connection_string: String,
},
/// An event emitted when the test case is assigned a follower node.
FollowerNodeAssigned {
/// A specifier for the test that the assignment is for.
test_specifier: Arc<TestSpecifier>,
/// The ID of the node that this case is being executed on.
id: usize,
/// The platform of the node.
platform: TestingPlatform,
/// The connection string of the node.
connection_string: String,
},
/// An event emitted by the runners when the compilation of the contracts has succeeded
/// on the pre-link contracts.
PreLinkContractsCompilationSucceeded {
/// A specifier for the execution that's taking place.
execution_specifier: Arc<ExecutionSpecifier>,
/// The version of the compiler used to compile the contracts.
compiler_version: Version,
/// The path of the compiler used to compile the contracts.
compiler_path: PathBuf,
/// A flag of whether the contract bytecode and ABI were cached or if they were compiled
/// anew.
is_cached: bool,
/// The input provided to the compiler - this is optional and not provided if the
/// contracts were obtained from the cache.
compiler_input: Option<CompilerInput>,
/// The output of the compiler.
compiler_output: CompilerOutput
},
/// An event emitted by the runners when the compilation of the contracts has succeeded
/// on the post-link contracts.
PostLinkContractsCompilationSucceeded {
/// A specifier for the execution that's taking place.
execution_specifier: Arc<ExecutionSpecifier>,
/// The version of the compiler used to compile the contracts.
compiler_version: Version,
/// The path of the compiler used to compile the contracts.
compiler_path: PathBuf,
/// A flag of whether the contract bytecode and ABI were cached or if they were compiled
/// anew.
is_cached: bool,
/// The input provided to the compiler - this is optional and not provided if the
/// contracts were obtained from the cache.
compiler_input: Option<CompilerInput>,
/// The output of the compiler.
compiler_output: CompilerOutput
},
/// An event emitted by the runners when the compilation of the pre-link contract has
/// failed.
PreLinkContractsCompilationFailed {
/// A specifier for the execution that's taking place.
execution_specifier: Arc<ExecutionSpecifier>,
/// The version of the compiler used to compile the contracts.
compiler_version: Option<Version>,
/// The path of the compiler used to compile the contracts.
compiler_path: Option<PathBuf>,
/// The input provided to the compiler - this is optional and not provided if the
/// contracts were obtained from the cache.
compiler_input: Option<CompilerInput>,
/// The failure reason.
reason: String,
},
/// An event emitted by the runners when the compilation of the post-link contract has
/// failed.
PostLinkContractsCompilationFailed {
/// A specifier for the execution that's taking place.
execution_specifier: Arc<ExecutionSpecifier>,
/// The version of the compiler used to compile the contracts.
compiler_version: Option<Version>,
/// The path of the compiler used to compile the contracts.
compiler_path: Option<PathBuf>,
/// The input provided to the compiler - this is optional and not provided if the
/// contracts were obtained from the cache.
compiler_input: Option<CompilerInput>,
/// The failure reason.
reason: String,
},
/// An event emitted by the runners when a library has been deployed.
LibrariesDeployed {
/// A specifier for the execution that's taking place.
execution_specifier: Arc<ExecutionSpecifier>,
/// The addresses of the libraries that were deployed.
libraries: BTreeMap<ContractInstance, Address>
},
/// An event emitted by the runners when they've deployed a new contract.
ContractDeployed {
/// A specifier for the execution that's taking place.
execution_specifier: Arc<ExecutionSpecifier>,
/// The instance name of the contract.
contract_instance: ContractInstance,
/// The address of the contract.
address: Address
},
}
}
/// An extension to the [`Reporter`] implemented by the macro.
impl RunnerEventReporter {
pub async fn subscribe(&self) -> anyhow::Result<broadcast::Receiver<ReporterEvent>> {
let (tx, rx) = oneshot::channel::<broadcast::Receiver<ReporterEvent>>();
self.report_subscribe_to_events_event(tx)?;
rx.await.map_err(Into::into)
}
}
pub type Reporter = RunnerEventReporter;
pub type TestSpecificReporter = RunnerEventTestSpecificReporter;
pub type ExecutionSpecificReporter = RunnerEventExecutionSpecificReporter;
+3
View File
@@ -19,3 +19,6 @@ reqwest = { workspace = true }
semver = { workspace = true } semver = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
sha2 = { workspace = true } sha2 = { workspace = true }
[lints]
workspace = true
+3 -6
View File
@@ -11,14 +11,14 @@ use std::{
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::download::GHDownloader; use crate::download::SolcDownloader;
pub const SOLC_CACHE_DIRECTORY: &str = "solc"; pub const SOLC_CACHE_DIRECTORY: &str = "solc";
pub(crate) static SOLC_CACHER: LazyLock<Mutex<HashSet<PathBuf>>> = LazyLock::new(Default::default); pub(crate) static SOLC_CACHER: LazyLock<Mutex<HashSet<PathBuf>>> = LazyLock::new(Default::default);
pub(crate) async fn get_or_download( pub(crate) async fn get_or_download(
working_directory: &Path, working_directory: &Path,
downloader: &GHDownloader, downloader: &SolcDownloader,
) -> anyhow::Result<PathBuf> { ) -> anyhow::Result<PathBuf> {
let target_directory = working_directory let target_directory = working_directory
.join(SOLC_CACHE_DIRECTORY) .join(SOLC_CACHE_DIRECTORY)
@@ -38,11 +38,8 @@ pub(crate) async fn get_or_download(
Ok(target_file) Ok(target_file)
} }
async fn download_to_file(path: &Path, downloader: &GHDownloader) -> anyhow::Result<()> { async fn download_to_file(path: &Path, downloader: &SolcDownloader) -> anyhow::Result<()> {
tracing::info!("caching file: {}", path.display());
let Ok(file) = File::create_new(path) else { let Ok(file) = File::create_new(path) else {
tracing::debug!("cache file already exists: {}", path.display());
return Ok(()); return Ok(());
}; };
+25 -25
View File
@@ -38,21 +38,21 @@ impl List {
} }
} }
/// Download solc binaries from GitHub releases (IPFS links aren't reliable). /// Download solc binaries from the official SolidityLang site
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct GHDownloader { pub struct SolcDownloader {
pub version: Version, pub version: Version,
pub target: &'static str, pub target: &'static str,
pub list: &'static str, pub list: &'static str,
} }
impl GHDownloader { impl SolcDownloader {
pub const BASE_URL: &str = "https://github.com/ethereum/solidity/releases/download"; pub const BASE_URL: &str = "https://binaries.soliditylang.org";
pub const LINUX_NAME: &str = "solc-static-linux"; pub const LINUX_NAME: &str = "linux-amd64";
pub const MACOSX_NAME: &str = "solc-macos"; pub const MACOSX_NAME: &str = "macosx-amd64";
pub const WINDOWS_NAME: &str = "solc-windows.exe"; pub const WINDOWS_NAME: &str = "windows-amd64";
pub const WASM_NAME: &str = "soljson.js"; pub const WASM_NAME: &str = "wasm";
async fn new( async fn new(
version: impl Into<VersionOrRequirement>, version: impl Into<VersionOrRequirement>,
@@ -102,26 +102,26 @@ impl GHDownloader {
Self::new(version, Self::WASM_NAME, List::WASM_URL).await Self::new(version, Self::WASM_NAME, List::WASM_URL).await
} }
/// Returns the download link.
pub fn url(&self) -> String {
format!("{}/v{}/{}", Self::BASE_URL, &self.version, &self.target)
}
/// Download the solc binary. /// Download the solc binary.
/// ///
/// Errors out if the download fails or the digest of the downloaded file /// Errors out if the download fails or the digest of the downloaded file
/// mismatches the expected digest from the release [List]. /// mismatches the expected digest from the release [List].
pub async fn download(&self) -> anyhow::Result<Vec<u8>> { pub async fn download(&self) -> anyhow::Result<Vec<u8>> {
tracing::info!("downloading solc: {self:?}"); let builds = List::download(self.list).await?.builds;
let expected_digest = List::download(self.list) let build = builds
.await?
.builds
.iter() .iter()
.find(|build| build.version == self.version) .find(|build| build.version == self.version)
.ok_or_else(|| anyhow::anyhow!("solc v{} not found builds", self.version)) .ok_or_else(|| anyhow::anyhow!("solc v{} not found builds", self.version))?;
.map(|b| b.sha256.strip_prefix("0x").unwrap_or(&b.sha256).to_string())?;
let file = reqwest::get(self.url()).await?.bytes().await?.to_vec(); let path = build.path.clone();
let expected_digest = build
.sha256
.strip_prefix("0x")
.unwrap_or(&build.sha256)
.to_string();
let url = format!("{}/{}/{}", Self::BASE_URL, self.target, path.display());
let file = reqwest::get(url).await?.bytes().await?.to_vec();
if hex::encode(Sha256::digest(&file)) != expected_digest { if hex::encode(Sha256::digest(&file)) != expected_digest {
anyhow::bail!("sha256 mismatch for solc version {}", self.version); anyhow::bail!("sha256 mismatch for solc version {}", self.version);
@@ -133,7 +133,7 @@ impl GHDownloader {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{download::GHDownloader, list::List}; use crate::{download::SolcDownloader, list::List};
#[tokio::test] #[tokio::test]
async fn try_get_windows() { async fn try_get_windows() {
@@ -141,7 +141,7 @@ mod tests {
.await .await
.unwrap() .unwrap()
.latest_release; .latest_release;
GHDownloader::windows(version) SolcDownloader::windows(version)
.await .await
.unwrap() .unwrap()
.download() .download()
@@ -155,7 +155,7 @@ mod tests {
.await .await
.unwrap() .unwrap()
.latest_release; .latest_release;
GHDownloader::macosx(version) SolcDownloader::macosx(version)
.await .await
.unwrap() .unwrap()
.download() .download()
@@ -169,7 +169,7 @@ mod tests {
.await .await
.unwrap() .unwrap()
.latest_release; .latest_release;
GHDownloader::linux(version) SolcDownloader::linux(version)
.await .await
.unwrap() .unwrap()
.download() .download()
@@ -180,7 +180,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn try_get_wasm() { async fn try_get_wasm() {
let version = List::download(List::WASM_URL).await.unwrap().latest_release; let version = List::download(List::WASM_URL).await.unwrap().latest_release;
GHDownloader::wasm(version) SolcDownloader::wasm(version)
.await .await
.unwrap() .unwrap()
.download() .download()
+5 -5
View File
@@ -6,7 +6,7 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use cache::get_or_download; use cache::get_or_download;
use download::GHDownloader; use download::SolcDownloader;
use revive_dt_common::types::VersionOrRequirement; use revive_dt_common::types::VersionOrRequirement;
@@ -25,13 +25,13 @@ pub async fn download_solc(
wasm: bool, wasm: bool,
) -> anyhow::Result<PathBuf> { ) -> anyhow::Result<PathBuf> {
let downloader = if wasm { let downloader = if wasm {
GHDownloader::wasm(version).await SolcDownloader::wasm(version).await
} else if cfg!(target_os = "linux") { } else if cfg!(target_os = "linux") {
GHDownloader::linux(version).await SolcDownloader::linux(version).await
} else if cfg!(target_os = "macos") { } else if cfg!(target_os = "macos") {
GHDownloader::macosx(version).await SolcDownloader::macosx(version).await
} else if cfg!(target_os = "windows") { } else if cfg!(target_os = "windows") {
GHDownloader::windows(version).await SolcDownloader::windows(version).await
} else { } else {
unimplemented!() unimplemented!()
}?; }?;