Compare commits

...

2 Commits

Author SHA1 Message Date
Omar Abdulla b4a06e1ac5 Update the readme 2025-08-19 20:40:35 +03: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
34 changed files with 966 additions and 737 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
+15 -1
View File
@@ -4482,6 +4482,7 @@ dependencies = [
"alloy", "alloy",
"alloy-primitives", "alloy-primitives",
"anyhow", "anyhow",
"dashmap",
"foundry-compilers-artifacts", "foundry-compilers-artifacts",
"revive-common", "revive-common",
"revive-dt-common", "revive-dt-common",
@@ -4532,6 +4533,7 @@ dependencies = [
"tempfile", "tempfile",
"tokio", "tokio",
"tracing", "tracing",
"tracing-appender",
"tracing-subscriber", "tracing-subscriber",
] ]
@@ -4543,6 +4545,7 @@ dependencies = [
"alloy-primitives", "alloy-primitives",
"alloy-sol-types", "alloy-sol-types",
"anyhow", "anyhow",
"futures",
"regex", "regex",
"revive-common", "revive-common",
"revive-dt-common", "revive-dt-common",
@@ -4592,7 +4595,6 @@ dependencies = [
"revive-dt-format", "revive-dt-format",
"serde", "serde",
"serde_json", "serde_json",
"tracing",
] ]
[[package]] [[package]]
@@ -6108,6 +6110,18 @@ dependencies = [
"tracing-core", "tracing-core",
] ]
[[package]]
name = "tracing-appender"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf"
dependencies = [
"crossbeam-channel",
"thiserror 1.0.69",
"time",
"tracing-subscriber",
]
[[package]] [[package]]
name = "tracing-attributes" name = "tracing-attributes"
version = "0.1.28" version = "0.1.28"
+5 -1
View File
@@ -28,6 +28,7 @@ anyhow = "1.0"
bson = { version = "2.15.0" } bson = { version = "2.15.0" }
cacache = { version = "13.1.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"
@@ -54,7 +55,8 @@ tokio = { version = "1.47.0", default-features = false, features = [
"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",
@@ -89,3 +91,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"
]
}
``` ```
+1
View File
@@ -0,0 +1 @@
+3
View File
@@ -15,3 +15,6 @@ once_cell = { workspace = true }
semver = { workspace = true } semver = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
tokio = { workspace = true, default-features = false, features = ["time"] } tokio = { workspace = true, default-features = false, features = ["time"] }
[lints]
workspace = true
@@ -1,3 +1,14 @@
#[macro_export]
macro_rules! impl_for_wrapper {
(Display, $ident: ident) => {
impl std::fmt::Display for $ident {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}
};
}
/// Defines wrappers around types. /// Defines wrappers around types.
/// ///
/// For example, the macro invocation seen below: /// For example, the macro invocation seen below:
@@ -42,7 +53,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 +115,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};
+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
+1 -1
View File
@@ -47,7 +47,7 @@ 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? /// Does the compiler support the provided mode and version settings?
fn supports_mode( fn supports_mode(
+20 -5
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::{
@@ -219,16 +221,23 @@ 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);
match VERSION_CACHE.entry(self.resolc_path.clone()) {
dashmap::Entry::Occupied(occupied_entry) => Ok(occupied_entry.get().clone()),
dashmap::Entry::Vacant(vacant_entry) => {
let output = Command::new(self.resolc_path.as_path()) let output = Command::new(self.resolc_path.as_path())
.arg("--version") .arg("--version")
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.spawn()? .spawn()?
.wait_with_output()? .wait_with_output()?
.stdout; .stdout;
let output = String::from_utf8_lossy(&output); let output = String::from_utf8_lossy(&output);
let version_string = output let version_string = output
.split("version ") .split("version ")
@@ -238,7 +247,13 @@ impl SolidityCompiler for Resolc {
.next() .next()
.context("Version parsing failed")?; .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( fn supports_mode(
@@ -268,7 +283,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");
+24 -12
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_dt_solc_binaries::download_solc; use revive_dt_solc_binaries::download_solc;
@@ -47,7 +49,7 @@ impl SolidityCompiler for Solc {
}: CompilerInput, }: CompilerInput,
_: Self::Options, _: Self::Options,
) -> anyhow::Result<CompilerOutput> { ) -> anyhow::Result<CompilerOutput> {
let compiler_supports_via_ir = self.version()? >= SOLC_VERSION_SUPPORTING_VIA_YUL_IR; 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, // 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 // as it will error if you provide fields it does not know about. Because
@@ -209,14 +211,22 @@ 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
/// not reused over time.
static VERSION_CACHE: LazyLock<DashMap<PathBuf, Version>> = LazyLock::new(Default::default);
match VERSION_CACHE.entry(self.solc_path.clone()) {
dashmap::Entry::Occupied(occupied_entry) => Ok(occupied_entry.get().clone()),
dashmap::Entry::Vacant(vacant_entry) => {
// The following is the parsing code for the version from the solc version strings
// which look like the following:
// ``` // ```
// solc, the solidity compiler commandline interface // solc, the solidity compiler commandline interface
// Version: 0.8.30+commit.73712a01.Darwin.appleclang // Version: 0.8.30+commit.73712a01.Darwin.appleclang
// ``` // ```
let child = Command::new(self.solc_path.as_path()) let child = Command::new(self.solc_path.as_path())
.arg("--version") .arg("--version")
.stdout(Stdio::piped()) .stdout(Stdio::piped())
@@ -232,7 +242,13 @@ impl SolidityCompiler for Solc {
.next() .next()
.context("Version parsing failed")?; .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( fn supports_mode(
@@ -256,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!(
@@ -277,15 +291,13 @@ mod test {
async fn compiler_version_can_be_obtained1() { async fn compiler_version_can_be_obtained1() {
// Arrange // Arrange
let args = Arguments::default(); let args = Arguments::default();
println!("Getting compiler path");
let path = Solc::get_compiler_executable(&args, Version::new(0, 4, 21)) let path = Solc::get_compiler_executable(&args, Version::new(0, 4, 21))
.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!(
-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
+4
View File
@@ -31,9 +31,13 @@ indexmap = { workspace = true }
once_cell = { 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 = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
temp-dir = { workspace = true } temp-dir = { workspace = true }
tempfile = { workspace = true } tempfile = { workspace = true }
[lints]
workspace = true
+3 -2
View File
@@ -62,8 +62,9 @@ impl CachedCompiler {
compiler_version_or_requirement, compiler_version_or_requirement,
) )
.await?; .await?;
let compiler_version = let compiler_version = <P::Compiler as SolidityCompiler>::new(compiler_path.clone())
<P::Compiler as SolidityCompiler>::new(compiler_path.clone()).version()?; .version()
.await?;
let cache_key = CacheKey { let cache_key = CacheKey {
platform_key: P::config_id().to_string(), platform_key: P::config_id().to_string(),
+84 -136
View File
@@ -16,26 +16,24 @@ use alloy::rpc::types::trace::geth::{
}; };
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_format::traits::{ResolutionContext, ResolverApi};
use semver::Version; use semver::Version;
use revive_dt_format::case::{Case, CaseIdx}; use revive_dt_format::case::Case;
use revive_dt_format::input::{ use revive_dt_format::input::{
BalanceAssertion, Calldata, EtherValue, Expected, ExpectedOutput, Input, Method, BalanceAssertion, Calldata, EtherValue, Expected, ExpectedOutput, Input, Method, StepIdx,
StorageEmptyAssertion, StorageEmptyAssertion,
}; };
use revive_dt_format::metadata::{ContractIdent, ContractInstance, ContractPathAndIdent}; use revive_dt_format::metadata::{ContractIdent, ContractInstance, ContractPathAndIdent};
use revive_dt_format::{input::Step, metadata::Metadata}; use revive_dt_format::{input::Step, metadata::Metadata};
use revive_dt_node::Node;
use revive_dt_node_interaction::EthereumNode; use revive_dt_node_interaction::EthereumNode;
use tracing::Instrument; use tokio::try_join;
use tracing::{Instrument, info, info_span, instrument};
use crate::Platform; use crate::Platform;
@@ -77,38 +75,38 @@ where
pub async fn handle_step( pub async fn handle_step(
&mut self, &mut self,
metadata: &Metadata, metadata: &Metadata,
case_idx: CaseIdx,
step: &Step, step: &Step,
node: &T::Blockchain, node: &T::Blockchain,
) -> anyhow::Result<StepOutput> { ) -> anyhow::Result<StepOutput> {
match step { match step {
Step::FunctionCall(input) => { Step::FunctionCall(input) => {
let (receipt, geth_trace, diff_mode) = let (receipt, geth_trace, diff_mode) =
self.handle_input(metadata, case_idx, input, node).await?; self.handle_input(metadata, input, node).await?;
Ok(StepOutput::FunctionCall(receipt, geth_trace, diff_mode)) Ok(StepOutput::FunctionCall(receipt, geth_trace, diff_mode))
} }
Step::BalanceAssertion(balance_assertion) => { Step::BalanceAssertion(balance_assertion) => {
self.handle_balance_assertion(metadata, case_idx, balance_assertion, node) self.handle_balance_assertion(metadata, balance_assertion, node)
.await?; .await?;
Ok(StepOutput::BalanceAssertion) Ok(StepOutput::BalanceAssertion)
} }
Step::StorageEmptyAssertion(storage_empty) => { Step::StorageEmptyAssertion(storage_empty) => {
self.handle_storage_empty(metadata, case_idx, storage_empty, node) self.handle_storage_empty(metadata, storage_empty, node)
.await?; .await?;
Ok(StepOutput::StorageEmptyAssertion) 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_input_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)
@@ -117,16 +115,17 @@ 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!(
.await?; self.handle_input_expectations(input, &execution_receipt, node, &tracing_result),
self.handle_input_diff(case_idx, execution_receipt, node) self.handle_input_diff(&execution_receipt, node)
.await )?;
Ok((execution_receipt, geth_trace, diff_mode))
} }
#[instrument(level = "info", name = "Handling Balance Assertion", skip_all)]
pub async fn handle_balance_assertion( pub async fn handle_balance_assertion(
&mut self, &mut self,
metadata: &Metadata, metadata: &Metadata,
_: CaseIdx,
balance_assertion: &BalanceAssertion, balance_assertion: &BalanceAssertion,
node: &T::Blockchain, node: &T::Blockchain,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
@@ -137,10 +136,10 @@ where
Ok(()) Ok(())
} }
#[instrument(level = "info", name = "Handling Storage Assertion", skip_all)]
pub async fn handle_storage_empty( pub async fn handle_storage_empty(
&mut self, &mut self,
metadata: &Metadata, metadata: &Metadata,
_: CaseIdx,
storage_empty: &StorageEmptyAssertion, storage_empty: &StorageEmptyAssertion,
node: &T::Blockchain, node: &T::Blockchain,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
@@ -152,10 +151,10 @@ where
} }
/// 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.
#[instrument(level = "info", skip_all)]
async fn handle_input_contract_deployment( async fn handle_input_contract_deployment(
&mut self, &mut self,
metadata: &Metadata, metadata: &Metadata,
_: CaseIdx,
input: &Input, input: &Input,
node: &T::Blockchain, node: &T::Blockchain,
) -> anyhow::Result<HashMap<ContractInstance, TransactionReceipt>> { ) -> anyhow::Result<HashMap<ContractInstance, TransactionReceipt>> {
@@ -170,11 +169,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);
@@ -201,6 +195,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,
@@ -218,33 +213,21 @@ where
.legacy_transaction(node, self.default_resolution_context()) .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,
@@ -259,7 +242,10 @@ where
tracer_config: GethDebugTracerConfig(serde_json::json! {{ tracer_config: GethDebugTracerConfig(serde_json::json! {{
"onlyTopCall": true, "onlyTopCall": true,
"withLog": false, "withLog": false,
"withReturnData": false "withStorage": false,
"withMemory": false,
"withStack": false,
"withReturnData": true
}}), }}),
..Default::default() ..Default::default()
}, },
@@ -272,6 +258,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,
@@ -302,8 +289,9 @@ where
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,
resolver: &impl ResolverApi, resolver: &impl ResolverApi,
@@ -337,24 +325,25 @@ where
} }
} }
for expectation in expectations.iter() { futures::stream::iter(expectations.into_iter().map(Ok))
.try_for_each_concurrent(None, |expectation| async move {
self.handle_input_expectation_item( self.handle_input_expectation_item(
execution_receipt, execution_receipt,
resolver, resolver,
expectation, expectation,
tracing_result, tracing_result,
) )
.await?; .await
} })
.await
Ok(())
} }
#[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,
resolver: &impl ResolverApi, 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 {
@@ -492,12 +481,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 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,
@@ -505,13 +494,14 @@ 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( pub async fn handle_balance_assertion_contract_deployment(
&mut self, &mut self,
metadata: &Metadata, metadata: &Metadata,
@@ -537,6 +527,7 @@ where
Ok(()) Ok(())
} }
#[instrument(level = "info", skip_all)]
pub async fn handle_balance_assertion_execution( pub async fn handle_balance_assertion_execution(
&mut self, &mut self,
BalanceAssertion { BalanceAssertion {
@@ -572,6 +563,7 @@ where
Ok(()) Ok(())
} }
#[instrument(level = "info", skip_all)]
pub async fn handle_storage_empty_assertion_contract_deployment( pub async fn handle_storage_empty_assertion_contract_deployment(
&mut self, &mut self,
metadata: &Metadata, metadata: &Metadata,
@@ -597,6 +589,7 @@ where
Ok(()) Ok(())
} }
#[instrument(level = "info", skip_all)]
pub async fn handle_storage_empty_assertion_execution( pub async fn handle_storage_empty_assertion_execution(
&mut self, &mut self,
StorageEmptyAssertion { StorageEmptyAssertion {
@@ -658,7 +651,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
@@ -671,11 +663,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
@@ -724,7 +711,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!(
@@ -751,7 +737,6 @@ where
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>,
@@ -767,7 +752,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>,
@@ -776,7 +760,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,
@@ -784,79 +767,44 @@ 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
.leader_node
.matches_target(self.metadata.targets.as_deref())
|| !self
.follower_node
.matches_target(self.metadata.targets.as_deref())
{
tracing::warn!(
targets = ?self.metadata.targets,
"Either the leader or follower node do not support the targets of the file"
);
return Ok(0);
}
let mut steps_executed = 0; let mut steps_executed = 0;
for (step_idx, step) in self.case.steps_iterator().enumerate() { for (step_idx, step) in self
let tracing_span = tracing::info_span!("Handling input", step_idx); .case
.steps_iterator()
.enumerate()
.map(|(idx, v)| (StepIdx::new(idx), v))
{
let (leader_step_output, follower_step_output) = try_join!(
self.leader_state
.handle_step(self.metadata, &step, self.leader_node)
.instrument(info_span!(
"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 leader_step_output = self
.leader_state
.handle_step(self.metadata, self.case_idx, &step, self.leader_node)
.instrument(tracing_span.clone())
.await?;
let follower_step_output = self
.follower_state
.handle_step(self.metadata, self.case_idx, &step, self.follower_node)
.instrument(tracing_span)
.await?;
match (leader_step_output, follower_step_output) { match (leader_step_output, follower_step_output) {
( (StepOutput::FunctionCall(..), StepOutput::FunctionCall(..)) => {
StepOutput::FunctionCall(leader_receipt, _, leader_diff), // TODO: We need to actually work out how/if we will compare the diff between
StepOutput::FunctionCall(follower_receipt, _, follower_diff), // 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
if leader_diff == follower_diff { // we have something that's guaranteed to fail. Even a simple call to some
tracing::debug!("State diffs match between leader and follower."); // contract will produce two non-equal diffs because on the leader the contract
} else { // has address X and on the follower it has address Y. On the leader contract X
tracing::debug!("State diffs mismatch between leader and follower."); // contains address A in the state and on the follower it contains address B. So
Self::trace_diff_mode("Leader", &leader_diff); // this isn't exactly a straightforward thing to do and I'm not even sure that
Self::trace_diff_mode("Follower", &follower_diff); // it's possible to do. Once we have an actual strategy for doing the diffs we
} // will implement it here. Until then, this remains empty.
if leader_receipt.logs() != follower_receipt.logs() {
tracing::debug!("Log/event mismatch between leader and follower.");
tracing::trace!("Leader logs: {:?}", leader_receipt.logs());
tracing::trace!("Follower logs: {:?}", follower_receipt.logs());
}
} }
(StepOutput::BalanceAssertion, StepOutput::BalanceAssertion) => {} (StepOutput::BalanceAssertion, StepOutput::BalanceAssertion) => {}
(StepOutput::StorageEmptyAssertion, StepOutput::StorageEmptyAssertion) => {} (StepOutput::StorageEmptyAssertion, StepOutput::StorageEmptyAssertion) => {}
+264 -198
View File
@@ -1,8 +1,9 @@
mod cached_compiler; mod cached_compiler;
use std::{ use std::{
collections::HashMap, collections::{BTreeMap, HashMap},
path::{Path, PathBuf}, io::{BufWriter, Write, stderr},
path::Path,
sync::{Arc, LazyLock}, sync::{Arc, LazyLock},
time::Instant, time::Instant,
}; };
@@ -13,16 +14,18 @@ use alloy::{
}; };
use anyhow::Context; use anyhow::Context;
use clap::Parser; use clap::Parser;
use futures::stream::futures_unordered::FuturesUnordered; use futures::stream;
use futures::{Stream, StreamExt}; use futures::{Stream, StreamExt};
use indexmap::IndexMap;
use revive_dt_node_interaction::EthereumNode; use revive_dt_node_interaction::EthereumNode;
use temp_dir::TempDir; use temp_dir::TempDir;
use tokio::sync::mpsc; use tokio::{sync::mpsc, try_join};
use tracing::{Instrument, Level}; use tracing::{debug, info, info_span, instrument};
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::{EnvFilter, FmtSubscriber}; use tracing_subscriber::{EnvFilter, FmtSubscriber};
use revive_dt_common::types::Mode; use revive_dt_common::types::Mode;
use revive_dt_compiler::SolidityCompiler; use revive_dt_compiler::{CompilerOutput, SolidityCompiler};
use revive_dt_config::*; use revive_dt_config::*;
use revive_dt_core::{ use revive_dt_core::{
Geth, Kitchensink, Platform, Geth, Kitchensink, Platform,
@@ -32,9 +35,10 @@ use revive_dt_format::{
case::{Case, CaseIdx}, case::{Case, CaseIdx},
corpus::Corpus, corpus::Corpus,
input::{Input, Step}, input::{Input, Step},
metadata::{ContractPathAndIdent, Metadata, MetadataFile}, metadata::{ContractPathAndIdent, MetadataFile},
mode::ParsedMode,
}; };
use revive_dt_node::pool::NodePool; use revive_dt_node::{Node, pool::NodePool};
use revive_dt_report::reporter::{Report, Span}; use revive_dt_report::reporter::{Report, Span};
use crate::cached_compiler::CachedCompiler; use crate::cached_compiler::CachedCompiler;
@@ -42,20 +46,28 @@ use crate::cached_compiler::CachedCompiler;
static TEMP_DIR: LazyLock<TempDir> = LazyLock::new(|| TempDir::new().unwrap()); static TEMP_DIR: LazyLock<TempDir> = LazyLock::new(|| TempDir::new().unwrap());
/// this represents a single "test"; a mode, path and collection of cases. /// this represents a single "test"; a mode, path and collection of cases.
#[derive(Clone)] #[derive(Clone, Debug)]
struct Test { struct Test<'a> {
metadata: Metadata, metadata: &'a MetadataFile,
path: PathBuf, metadata_file_path: &'a Path,
mode: Mode, mode: Mode,
case_idx: CaseIdx, case_idx: CaseIdx,
case: Case, case: &'a Case,
} }
/// This represents the results that we gather from running test cases. /// This represents the results that we gather from running test cases.
type CaseResult = Result<usize, anyhow::Error>; type CaseResult = Result<usize, anyhow::Error>;
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {
let args = init_cli()?; let (args, _guard) = init_cli()?;
info!(
leader = args.leader.to_string(),
follower = args.follower.to_string(),
working_directory = %args.directory().display(),
number_of_nodes = args.number_of_nodes,
invalidate_compilation_cache = args.invalidate_compilation_cache,
"Differential testing tool has been initialized"
);
let body = async { let body = async {
for (corpus, tests) in collect_corpora(&args)? { for (corpus, tests) in collect_corpora(&args)? {
@@ -77,15 +89,25 @@ fn main() -> anyhow::Result<()> {
.block_on(body) .block_on(body)
} }
fn init_cli() -> anyhow::Result<Arguments> { fn init_cli() -> anyhow::Result<(Arguments, WorkerGuard)> {
let (writer, guard) = tracing_appender::non_blocking::NonBlockingBuilder::default()
.lossy(false)
// Assuming that each line contains 255 characters and that each character is one byte, then
// this means that our buffer is about 4GBs large.
.buffered_lines_limit(0x1000000)
.thread_name("buffered writer")
.finish(std::io::stdout());
let subscriber = FmtSubscriber::builder() let subscriber = FmtSubscriber::builder()
.with_thread_ids(true) .with_writer(writer)
.with_thread_names(true) .with_thread_ids(false)
.with_thread_names(false)
.with_env_filter(EnvFilter::from_default_env()) .with_env_filter(EnvFilter::from_default_env())
.with_ansi(false) .with_ansi(false)
.pretty() .pretty()
.finish(); .finish();
tracing::subscriber::set_global_default(subscriber)?; tracing::subscriber::set_global_default(subscriber)?;
info!("Differential testing tool is starting");
let mut args = Arguments::parse(); let mut args = Arguments::parse();
@@ -103,19 +125,25 @@ fn init_cli() -> anyhow::Result<Arguments> {
args.temp_dir = Some(&TEMP_DIR); args.temp_dir = Some(&TEMP_DIR);
} }
} }
tracing::info!("workdir: {}", args.directory().display());
Ok(args) Ok((args, guard))
} }
#[instrument(level = "debug", name = "Collecting Corpora", skip_all)]
fn collect_corpora(args: &Arguments) -> anyhow::Result<HashMap<Corpus, Vec<MetadataFile>>> { fn collect_corpora(args: &Arguments) -> anyhow::Result<HashMap<Corpus, Vec<MetadataFile>>> {
let mut corpora = HashMap::new(); let mut corpora = HashMap::new();
for path in &args.corpus { for path in &args.corpus {
let span = info_span!("Processing corpus file", path = %path.display());
let _guard = span.enter();
let corpus = Corpus::try_from_path(path)?; let corpus = Corpus::try_from_path(path)?;
tracing::info!("found corpus: {}", path.display()); info!(
name = corpus.name(),
number_of_contained_paths = corpus.path_count(),
"Deserialized corpus file"
);
let tests = corpus.enumerate_tests(); let tests = corpus.enumerate_tests();
tracing::info!("corpus '{}' contains {} tests", &corpus.name(), tests.len());
corpora.insert(corpus, tests); corpora.insert(corpus, tests);
} }
@@ -133,7 +161,7 @@ where
L::Blockchain: revive_dt_node::Node + Send + Sync + 'static, L::Blockchain: revive_dt_node::Node + Send + Sync + 'static,
F::Blockchain: revive_dt_node::Node + Send + Sync + 'static, F::Blockchain: revive_dt_node::Node + Send + Sync + 'static,
{ {
let (report_tx, report_rx) = mpsc::unbounded_channel::<(Test, CaseResult)>(); let (report_tx, report_rx) = mpsc::unbounded_channel::<(Test<'_>, CaseResult)>();
let tests = prepare_tests::<L, F>(args, metadata_files); let tests = prepare_tests::<L, F>(args, metadata_files);
let driver_task = start_driver_task::<L, F>(args, tests, span, report_tx).await?; let driver_task = start_driver_task::<L, F>(args, tests, span, report_tx).await?;
@@ -144,111 +172,148 @@ where
Ok(()) Ok(())
} }
fn prepare_tests<L, F>( fn prepare_tests<'a, L, F>(
args: &Arguments, args: &Arguments,
metadata_files: &[MetadataFile], metadata_files: &'a [MetadataFile],
) -> impl Stream<Item = Test> ) -> impl Stream<Item = Test<'a>>
where where
L: Platform, L: Platform,
F: Platform, F: Platform,
L::Blockchain: revive_dt_node::Node + Send + Sync + 'static, L::Blockchain: revive_dt_node::Node + Send + Sync + 'static,
F::Blockchain: revive_dt_node::Node + Send + Sync + 'static, F::Blockchain: revive_dt_node::Node + Send + Sync + 'static,
{ {
metadata_files let filtered_tests = metadata_files
.iter() .iter()
.flat_map( .flat_map(|metadata_file| {
|MetadataFile { metadata_file
path,
content: metadata,
}| {
metadata
.cases .cases
.iter() .iter()
.enumerate() .enumerate()
.flat_map(move |(case_idx, case)| { .map(move |(case_idx, case)| (metadata_file, case_idx, case))
metadata
.solc_modes()
.into_iter()
.map(move |solc_mode| (path, metadata, case_idx, case, solc_mode))
}) })
// Flatten over the modes, prefer the case modes over the metadata file modes.
.flat_map(|(metadata_file, case_idx, case)| {
case.modes
.as_ref()
.or(metadata_file.modes.as_ref())
.map(|modes| ParsedMode::many_to_modes(modes.iter()).collect::<Vec<_>>())
.unwrap_or(Mode::all().collect())
.into_iter()
.map(move |mode| (metadata_file, case_idx, case, mode))
})
.fold(
IndexMap::<_, BTreeMap<_, Vec<_>>>::new(),
|mut map, (metadata_file, case_idx, case, mode)| {
let test = Test {
metadata: metadata_file,
metadata_file_path: metadata_file.metadata_file_path.as_path(),
mode: mode.clone(),
case_idx: CaseIdx::new(case_idx),
case,
};
map.entry(mode)
.or_default()
.entry(test.case_idx)
.or_default()
.push(test);
map
}, },
) )
.filter( .into_values()
|(metadata_file_path, metadata, _, _, _)| match metadata.ignore { .flatten()
Some(true) => { .flat_map(|(_, value)| value.into_iter())
tracing::warn!( // Filter the test out if the leader and follower do not support the target.
metadata_file_path = %metadata_file_path.display(), .filter(|test| {
"Ignoring metadata file" let leader_support =
); <L::Blockchain as Node>::matches_target(test.metadata.targets.as_deref());
false let follower_support =
} <F::Blockchain as Node>::matches_target(test.metadata.targets.as_deref());
Some(false) | None => true, let is_allowed = leader_support && follower_support;
},
)
.filter(
|(metadata_file_path, _, case_idx, case, _)| match case.ignore {
Some(true) => {
tracing::warn!(
metadata_file_path = %metadata_file_path.display(),
case_idx,
case_name = ?case.name,
"Ignoring case"
);
false
}
Some(false) | None => true,
},
)
.filter(|(metadata_file_path, metadata, ..)| match metadata.required_evm_version {
Some(evm_version_requirement) => {
let is_allowed = evm_version_requirement
.matches(&<L::Blockchain as revive_dt_node::Node>::evm_version())
&& evm_version_requirement
.matches(&<F::Blockchain as revive_dt_node::Node>::evm_version());
if !is_allowed { if !is_allowed {
tracing::warn!( debug!(
metadata_file_path = %metadata_file_path.display(), file_path = %test.metadata.relative_path().display(),
leader_evm_version = %<L::Blockchain as revive_dt_node::Node>::evm_version(), leader_support,
follower_evm_version = %<F::Blockchain as revive_dt_node::Node>::evm_version(), follower_support,
version_requirement = %evm_version_requirement, "Target is not supported, throwing metadata file out"
"Skipped test since the EVM version requirement was not fulfilled." )
}
is_allowed
})
// Filter the test out if the metadata file is ignored.
.filter(|test| {
if test.metadata.ignore.is_some_and(|ignore| ignore) {
debug!(
file_path = %test.metadata.relative_path().display(),
"Metadata file is ignored, throwing case out"
);
false
} else {
true
}
})
// Filter the test case if the case is ignored.
.filter(|test| {
if test.case.ignore.is_some_and(|ignore| ignore) {
debug!(
file_path = %test.metadata.relative_path().display(),
case_idx = %test.case_idx,
"Case is ignored, throwing case out"
);
false
} else {
true
}
})
// Filtering based on the EVM version compatibility
.filter(|test| {
if let Some(evm_version_requirement) = test.metadata.required_evm_version {
let leader_compatibility = evm_version_requirement
.matches(&<L::Blockchain as revive_dt_node::Node>::evm_version());
let follower_compatibility = evm_version_requirement
.matches(&<F::Blockchain as revive_dt_node::Node>::evm_version());
let is_allowed = leader_compatibility && follower_compatibility;
if !is_allowed {
debug!(
file_path = %test.metadata.relative_path().display(),
case_idx = %test.case_idx,
leader_compatibility,
follower_compatibility,
"EVM Version is incompatible, throwing case out"
); );
} }
is_allowed is_allowed
}
None => true,
})
.map(|(metadata_file_path, metadata, case_idx, case, solc_mode)| {
Test {
metadata: metadata.clone(),
path: metadata_file_path.to_path_buf(),
mode: solc_mode,
case_idx: case_idx.into(),
case: case.clone(),
}
})
.map(async |test| test)
.collect::<FuturesUnordered<_>>()
.filter_map(async move |test| {
// Check that both compilers support this test, else we skip it
let is_supported = does_compiler_support_mode::<L>(args, &test.mode).await.ok().unwrap_or(false) &&
does_compiler_support_mode::<F>(args, &test.mode).await.ok().unwrap_or(false);
// We filter_map to avoid needing to clone `test`, but return it as-is.
if is_supported {
Some(test)
} else { } else {
tracing::warn!( true
metadata_file_path = %test.path.display(),
case_idx = %test.case_idx,
case_name = ?test.case.name,
mode = %test.mode,
"Skipping test as one or both of the compilers don't support it"
);
None
} }
});
stream::iter(filtered_tests)
// Filter based on the compiler compatibility
.filter_map(move |test| async move {
let leader_support = does_compiler_support_mode::<L>(args, &test.mode)
.await
.ok()
.unwrap_or(false);
let follower_support = does_compiler_support_mode::<F>(args, &test.mode)
.await
.ok()
.unwrap_or(false);
let is_allowed = leader_support && follower_support;
if !is_allowed {
debug!(
file_path = %test.metadata.relative_path().display(),
leader_support,
follower_support,
"Compilers do not support this, throwing case out"
);
}
is_allowed.then_some(test)
}) })
} }
@@ -259,7 +324,7 @@ async fn does_compiler_support_mode<P: Platform>(
let compiler_version_or_requirement = mode.compiler_version_to_use(args.solc.clone()); let compiler_version_or_requirement = mode.compiler_version_to_use(args.solc.clone());
let compiler_path = let compiler_path =
P::Compiler::get_compiler_executable(args, compiler_version_or_requirement).await?; P::Compiler::get_compiler_executable(args, compiler_version_or_requirement).await?;
let compiler_version = P::Compiler::new(compiler_path.clone()).version()?; let compiler_version = P::Compiler::new(compiler_path.clone()).version().await?;
Ok(P::Compiler::supports_mode( Ok(P::Compiler::supports_mode(
&compiler_version, &compiler_version,
@@ -268,11 +333,11 @@ async fn does_compiler_support_mode<P: Platform>(
)) ))
} }
async fn start_driver_task<L, F>( async fn start_driver_task<'a, L, F>(
args: &Arguments, args: &Arguments,
tests: impl Stream<Item = Test>, tests: impl Stream<Item = Test<'a>>,
span: Span, span: Span,
report_tx: mpsc::UnboundedSender<(Test, CaseResult)>, report_tx: mpsc::UnboundedSender<(Test<'a>, CaseResult)>,
) -> anyhow::Result<impl Future<Output = ()>> ) -> anyhow::Result<impl Future<Output = ()>>
where where
L: Platform, L: Platform,
@@ -310,19 +375,11 @@ where
let leader_node = leader_nodes.round_robbin(); let leader_node = leader_nodes.round_robbin();
let follower_node = follower_nodes.round_robbin(); let follower_node = follower_nodes.round_robbin();
let tracing_span = tracing::span!(
Level::INFO,
"Running driver",
metadata_file_path = %test.path.display(),
case_idx = ?test.case_idx,
solc_mode = ?test.mode,
);
let result = handle_case_driver::<L, F>( let result = handle_case_driver::<L, F>(
&test.path, test.metadata_file_path,
&test.metadata, test.metadata,
test.case_idx, test.case_idx,
&test.case, test.case,
test.mode.clone(), test.mode.clone(),
args, args,
cached_compiler, cached_compiler,
@@ -330,7 +387,6 @@ where
follower_node, follower_node,
span, span,
) )
.instrument(tracing_span)
.await; .await;
report_tx report_tx
@@ -341,7 +397,7 @@ where
)) ))
} }
async fn start_reporter_task(mut report_rx: mpsc::UnboundedReceiver<(Test, CaseResult)>) { async fn start_reporter_task(mut report_rx: mpsc::UnboundedReceiver<(Test<'_>, CaseResult)>) {
let start = Instant::now(); let start = Instant::now();
const GREEN: &str = "\x1B[32m"; const GREEN: &str = "\x1B[32m";
@@ -355,22 +411,25 @@ async fn start_reporter_task(mut report_rx: mpsc::UnboundedReceiver<(Test, CaseR
let mut failures = vec![]; let mut failures = vec![];
// Wait for reports to come from our test runner. When the channel closes, this ends. // Wait for reports to come from our test runner. When the channel closes, this ends.
let mut buf = BufWriter::new(stderr());
while let Some((test, case_result)) = report_rx.recv().await { while let Some((test, case_result)) = report_rx.recv().await {
let case_name = test.case.name.as_deref().unwrap_or("unnamed_case"); let case_name = test.case.name.as_deref().unwrap_or("unnamed_case");
let case_idx = test.case_idx; let case_idx = test.case_idx;
let test_path = test.path.display(); let test_path = test.metadata_file_path.display();
let test_mode = test.mode.clone(); let test_mode = test.mode.clone();
match case_result { match case_result {
Ok(_inputs) => { Ok(_inputs) => {
number_of_successes += 1; number_of_successes += 1;
eprintln!( let _ = writeln!(
buf,
"{GREEN}Case Succeeded:{COLOUR_RESET} {test_path} -> {case_name}:{case_idx} (mode: {test_mode})" "{GREEN}Case Succeeded:{COLOUR_RESET} {test_path} -> {case_name}:{case_idx} (mode: {test_mode})"
); );
} }
Err(err) => { Err(err) => {
number_of_failures += 1; number_of_failures += 1;
eprintln!( let _ = writeln!(
buf,
"{RED}Case Failed:{COLOUR_RESET} {test_path} -> {case_name}:{case_idx} (mode: {test_mode})" "{RED}Case Failed:{COLOUR_RESET} {test_path} -> {case_name}:{case_idx} (mode: {test_mode})"
); );
failures.push((test, err)); failures.push((test, err));
@@ -378,29 +437,31 @@ async fn start_reporter_task(mut report_rx: mpsc::UnboundedReceiver<(Test, CaseR
} }
} }
eprintln!(); let _ = writeln!(buf,);
let elapsed = start.elapsed(); let elapsed = start.elapsed();
// Now, log the failures with more complete errors at the bottom, like `cargo test` does, so // Now, log the failures with more complete errors at the bottom, like `cargo test` does, so
// that we don't have to scroll through the entire output to find them. // that we don't have to scroll through the entire output to find them.
if !failures.is_empty() { if !failures.is_empty() {
eprintln!("{BOLD}Failures:{BOLD_RESET}\n"); let _ = writeln!(buf, "{BOLD}Failures:{BOLD_RESET}\n");
for failure in failures { for failure in failures {
let (test, err) = failure; let (test, err) = failure;
let case_name = test.case.name.as_deref().unwrap_or("unnamed_case"); let case_name = test.case.name.as_deref().unwrap_or("unnamed_case");
let case_idx = test.case_idx; let case_idx = test.case_idx;
let test_path = test.path.display(); let test_path = test.metadata_file_path.display();
let test_mode = test.mode.clone(); let test_mode = test.mode.clone();
eprintln!( let _ = writeln!(
buf,
"---- {RED}Case Failed:{COLOUR_RESET} {test_path} -> {case_name}:{case_idx} (mode: {test_mode}) ----\n\n{err}\n" "---- {RED}Case Failed:{COLOUR_RESET} {test_path} -> {case_name}:{case_idx} (mode: {test_mode}) ----\n\n{err}\n"
); );
} }
} }
// Summary at the end. // Summary at the end.
eprintln!( let _ = writeln!(
buf,
"{} cases: {GREEN}{number_of_successes}{COLOUR_RESET} cases succeeded, {RED}{number_of_failures}{COLOUR_RESET} cases failed in {} seconds", "{} cases: {GREEN}{number_of_successes}{COLOUR_RESET} cases succeeded, {RED}{number_of_failures}{COLOUR_RESET} cases failed in {} seconds",
number_of_successes + number_of_failures, number_of_successes + number_of_failures,
elapsed.as_secs() elapsed.as_secs()
@@ -408,9 +469,22 @@ async fn start_reporter_task(mut report_rx: mpsc::UnboundedReceiver<(Test, CaseR
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
#[instrument(
level = "info",
name = "Handling Case"
skip_all,
fields(
metadata_file_path = %metadata.relative_path().display(),
mode = %mode,
%case_idx,
case_name = case.name.as_deref().unwrap_or("Unnamed Case"),
leader_node = leader_node.id(),
follower_node = follower_node.id(),
)
)]
async fn handle_case_driver<L, F>( async fn handle_case_driver<L, F>(
metadata_file_path: &Path, metadata_file_path: &Path,
metadata: &Metadata, metadata: &MetadataFile,
case_idx: CaseIdx, case_idx: CaseIdx,
case: &Case, case: &Case,
mode: Mode, mode: Mode,
@@ -426,16 +500,23 @@ where
L::Blockchain: revive_dt_node::Node + Send + Sync + 'static, L::Blockchain: revive_dt_node::Node + Send + Sync + 'static,
F::Blockchain: revive_dt_node::Node + Send + Sync + 'static, F::Blockchain: revive_dt_node::Node + Send + Sync + 'static,
{ {
let leader_pre_link_contracts = cached_compiler let (
.compile_contracts::<L>(metadata, metadata_file_path, &mode, config, None) (
.await? CompilerOutput {
.0 contracts: leader_pre_link_contracts,
.contracts; },
let follower_pre_link_contracts = cached_compiler _,
.compile_contracts::<F>(metadata, metadata_file_path, &mode, config, None) ),
.await? (
.0 CompilerOutput {
.contracts; contracts: follower_pre_link_contracts,
},
_,
),
) = try_join!(
cached_compiler.compile_contracts::<L>(metadata, metadata_file_path, &mode, config, None),
cached_compiler.compile_contracts::<F>(metadata, metadata_file_path, &mode, config, None)
)?;
let mut leader_deployed_libraries = None::<HashMap<_, _>>; let mut leader_deployed_libraries = None::<HashMap<_, _>>;
let mut follower_deployed_libraries = None::<HashMap<_, _>>; let mut follower_deployed_libraries = None::<HashMap<_, _>>;
@@ -446,6 +527,8 @@ where
.flatten() .flatten()
.flat_map(|(_, map)| map.values()) .flat_map(|(_, map)| map.values())
{ {
debug!(%library_instance, "Deploying Library Instance");
let ContractPathAndIdent { let ContractPathAndIdent {
contract_source_path: library_source_path, contract_source_path: library_source_path,
contract_ident: library_ident, contract_ident: library_ident,
@@ -465,24 +548,12 @@ where
let leader_code = match alloy::hex::decode(leader_code) { let leader_code = match alloy::hex::decode(leader_code) {
Ok(code) => code, Ok(code) => code,
Err(error) => { Err(error) => {
tracing::error!(
?error,
contract_source_path = library_source_path.display().to_string(),
contract_ident = library_ident.as_ref(),
"Failed to hex-decode byte code - This could possibly mean that the bytecode requires linking"
);
anyhow::bail!("Failed to hex-decode the byte code {}", error) anyhow::bail!("Failed to hex-decode the byte code {}", error)
} }
}; };
let follower_code = match alloy::hex::decode(follower_code) { let follower_code = match alloy::hex::decode(follower_code) {
Ok(code) => code, Ok(code) => code,
Err(error) => { Err(error) => {
tracing::error!(
?error,
contract_source_path = library_source_path.display().to_string(),
contract_ident = library_ident.as_ref(),
"Failed to hex-decode byte code - This could possibly mean that the bytecode requires linking"
);
anyhow::bail!("Failed to hex-decode the byte code {}", error) anyhow::bail!("Failed to hex-decode the byte code {}", error)
} }
}; };
@@ -509,46 +580,28 @@ where
follower_code, follower_code,
); );
let leader_receipt = match leader_node.execute_transaction(leader_tx).await { let (leader_receipt, follower_receipt) = try_join!(
Ok(receipt) => receipt, leader_node.execute_transaction(leader_tx),
Err(error) => { follower_node.execute_transaction(follower_tx)
tracing::error!( )?;
node = std::any::type_name::<L>(),
?error,
"Contract deployment transaction failed."
);
return Err(error);
}
};
let follower_receipt = match follower_node.execute_transaction(follower_tx).await {
Ok(receipt) => receipt,
Err(error) => {
tracing::error!(
node = std::any::type_name::<F>(),
?error,
"Contract deployment transaction failed."
);
return Err(error);
}
};
tracing::info!( debug!(
?library_instance, ?library_instance,
library_address = ?leader_receipt.contract_address, library_address = ?leader_receipt.contract_address,
"Deployed library to leader" "Deployed library to leader"
); );
tracing::info!( debug!(
?library_instance, ?library_instance,
library_address = ?follower_receipt.contract_address, library_address = ?follower_receipt.contract_address,
"Deployed library to follower" "Deployed library to follower"
); );
let Some(leader_library_address) = leader_receipt.contract_address else { let leader_library_address = leader_receipt
anyhow::bail!("Contract deployment didn't return an address"); .contract_address
}; .context("Contract deployment didn't return an address")?;
let Some(follower_library_address) = follower_receipt.contract_address else { let follower_library_address = follower_receipt
anyhow::bail!("Contract deployment didn't return an address"); .contract_address
}; .context("Contract deployment didn't return an address")?;
leader_deployed_libraries.get_or_insert_default().insert( leader_deployed_libraries.get_or_insert_default().insert(
library_instance.clone(), library_instance.clone(),
@@ -568,46 +621,59 @@ where
); );
} }
let (leader_post_link_contracts, leader_compiler_version) = cached_compiler let (
.compile_contracts::<L>( (
CompilerOutput {
contracts: leader_post_link_contracts,
},
leader_compiler_version,
),
(
CompilerOutput {
contracts: follower_post_link_contracts,
},
follower_compiler_version,
),
) = try_join!(
cached_compiler.compile_contracts::<L>(
metadata, metadata,
metadata_file_path, metadata_file_path,
&mode, &mode,
config, config,
leader_deployed_libraries.as_ref(), leader_deployed_libraries.as_ref()
) ),
.await?; cached_compiler.compile_contracts::<F>(
let (follower_post_link_contracts, follower_compiler_version) = cached_compiler
.compile_contracts::<F>(
metadata, metadata,
metadata_file_path, metadata_file_path,
&mode, &mode,
config, config,
follower_deployed_libraries.as_ref(), follower_deployed_libraries.as_ref()
) )
.await?; )?;
let leader_state = CaseState::<L>::new( let leader_state = CaseState::<L>::new(
leader_compiler_version, leader_compiler_version,
leader_post_link_contracts.contracts, leader_post_link_contracts,
leader_deployed_libraries.unwrap_or_default(), leader_deployed_libraries.unwrap_or_default(),
); );
let follower_state = CaseState::<F>::new( let follower_state = CaseState::<F>::new(
follower_compiler_version, follower_compiler_version,
follower_post_link_contracts.contracts, follower_post_link_contracts,
follower_deployed_libraries.unwrap_or_default(), follower_deployed_libraries.unwrap_or_default(),
); );
let mut driver = CaseDriver::<L, F>::new( let mut driver = CaseDriver::<L, F>::new(
metadata, metadata,
case, case,
case_idx,
leader_node, leader_node,
follower_node, follower_node,
leader_state, leader_state,
follower_state, follower_state,
); );
driver.execute().await driver
.execute()
.await
.inspect(|steps_executed| info!(steps_executed, "Case succeeded"))
} }
async fn execute_corpus( async fn execute_corpus(
@@ -657,7 +723,7 @@ async fn compile_corpus(
let _ = cached_compiler let _ = cached_compiler
.compile_contracts::<Geth>( .compile_contracts::<Geth>(
metadata, metadata,
metadata.path.as_path(), metadata.metadata_file_path.as_path(),
&mode, &mode,
config, config,
None, None,
@@ -668,7 +734,7 @@ async fn compile_corpus(
let _ = cached_compiler let _ = cached_compiler
.compile_contracts::<Kitchensink>( .compile_contracts::<Kitchensink>(
metadata, metadata,
metadata.path.as_path(), metadata.metadata_file_path.as_path(),
&mode, &mode,
config, config,
None, None,
+4
View File
@@ -17,6 +17,7 @@ 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 } regex = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
semver = { workspace = true } semver = { workspace = true }
@@ -25,3 +26,6 @@ serde_json = { workspace = true }
[dev-dependencies] [dev-dependencies]
tokio = { workspace = true } tokio = { workspace = true }
[lints]
workspace = true
+9 -8
View File
@@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize}; 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, Step}, input::{Expected, Step},
@@ -60,16 +60,17 @@ impl Case {
} }
}) })
} }
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)]
pub struct CaseIdx(usize); pub struct CaseIdx(usize) impl Display;
); );
impl std::fmt::Display for CaseIdx {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
+53 -54
View File
@@ -3,10 +3,11 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use revive_dt_common::cached_fs::read_dir; 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, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(untagged)] #[serde(untagged)]
@@ -18,7 +19,7 @@ pub enum Corpus {
impl Corpus { impl Corpus {
pub fn try_from_path(file_path: impl AsRef<Path>) -> anyhow::Result<Self> { pub fn try_from_path(file_path: impl AsRef<Path>) -> anyhow::Result<Self> {
let mut corpus = File::open(file_path.as_ref()) let mut corpus = File::open(file_path.as_ref())
.map_err(Into::<anyhow::Error>::into) .map_err(anyhow::Error::from)
.and_then(|file| serde_json::from_reader::<_, Corpus>(file).map_err(Into::into))?; .and_then(|file| serde_json::from_reader::<_, Corpus>(file).map_err(Into::into))?;
for path in corpus.paths_iter_mut() { for path in corpus.paths_iter_mut() {
@@ -42,10 +43,52 @@ impl Corpus {
} }
pub fn enumerate_tests(&self) -> Vec<MetadataFile> { pub fn enumerate_tests(&self) -> Vec<MetadataFile> {
let mut tests = Vec::new(); let mut tests = self
for path in self.paths_iter() { .paths_iter()
collect_metadata(path, &mut tests); .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
} }
@@ -76,55 +119,11 @@ impl Corpus {
} }
} }
} }
}
/// Recursively walks `path` and parses any JSON or Solidity file into a test pub fn path_count(&self) -> usize {
/// definition [Metadata]. match self {
/// Corpus::SinglePath { .. } => 1,
/// Found tests are inserted into `tests`. Corpus::MultiplePaths { paths, .. } => paths.len(),
///
/// `path` is expected to be a directory.
pub fn collect_metadata(path: &Path, tests: &mut Vec<MetadataFile>) {
if path.is_dir() {
let dir_entry = match read_dir(path) {
Ok(dir_entry) => dir_entry,
Err(error) => {
tracing::error!("failed to read dir '{}': {error}", path.display());
return;
}
};
for path in dir_entry {
let path = match path {
Ok(entry) => entry,
Err(error) => {
tracing::error!("error reading dir entry: {error}");
continue;
}
};
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");
return;
};
if extension.eq_ignore_ascii_case("sol") || extension.eq_ignore_ascii_case("json") {
if let Some(metadata) = MetadataFile::try_from_file(path) {
tests.push(metadata)
}
} else {
tracing::error!(?extension, "Unsupported file extension");
} }
} }
} }
+32 -44
View File
@@ -2,7 +2,6 @@ use std::collections::HashMap;
use alloy::{ use alloy::{
eips::BlockNumberOrTag, eips::BlockNumberOrTag,
hex::ToHexExt,
json_abi::Function, json_abi::Function,
network::TransactionBuilder, network::TransactionBuilder,
primitives::{Address, Bytes, U256}, primitives::{Address, Bytes, U256},
@@ -10,10 +9,12 @@ use alloy::{
}; };
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::traits::ResolverApi; use crate::traits::ResolverApi;
use crate::{metadata::ContractInstance, traits::ResolutionContext}; use crate::{metadata::ContractInstance, traits::ResolutionContext};
@@ -33,6 +34,11 @@ pub enum Step {
StorageEmptyAssertion(Box<StorageEmptyAssertion>), 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)] #[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
pub struct Input { pub struct Input {
#[serde(default = "Input::default_caller")] #[serde(default = "Input::default_caller")]
@@ -188,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)]
@@ -233,7 +239,7 @@ 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, Serialize, Deserialize, Eq, PartialEq)] #[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
@@ -268,15 +274,9 @@ impl Input {
} }
Method::FunctionName(ref function_name) => { Method::FunctionName(ref function_name) => {
let Some(abi) = context.deployed_contract_abi(&self.instance) else { let Some(abi) = context.deployed_contract_abi(&self.instance) else {
tracing::error!(
contract_name = self.instance.as_ref(),
"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.
@@ -302,13 +302,6 @@ impl Input {
.selector() .selector()
}; };
tracing::trace!("Functions found for instance: {}", self.instance.as_ref());
tracing::trace!(
"Starting encoding ABI's parameters for instance: {}",
self.instance.as_ref()
);
// 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.
// function.inputs.len() * 32 bytes for the arguments (each argument is a U256). // function.inputs.len() * 32 bytes for the arguments (each argument is a U256).
@@ -435,17 +428,18 @@ impl Calldata {
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.resolve(resolver, context).await { .map(|(arg_idx, arg)| async move {
Ok(resolved) => { arg.resolve(resolver, context)
buffer.extend(resolved.to_be_bytes::<32>()); .instrument(info_span!("Resolving argument", %arg, arg_idx))
} .map_ok(|value| value.to_be_bytes::<32>())
Err(error) => { .await
tracing::error!(?arg, arg_idx, ?error, "Failed to resolve argument"); })
return Err(error); .buffered(0xFF)
} .try_collect::<Vec<_>>()
}; .await?;
}
buffer.extend(resolved.into_iter().flatten());
} }
}; };
Ok(()) Ok(())
@@ -468,13 +462,12 @@ impl Calldata {
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() == "*" {
continue; return Ok::<_, anyhow::Error>(true);
} }
let other = if other.len() < 32 { let other = if other.len() < 32 {
@@ -487,17 +480,19 @@ impl Calldata {
let this = this.resolve(resolver, context).await?; let this = this.resolve(resolver, context).await?;
let other = U256::from_be_slice(&other); let other = U256::from_be_slice(&other);
if this != other { Ok(this == other)
return Ok(false); })
} .buffered(0xFF)
} .all(|v| async move { v.is_ok_and(|v| v) })
Ok(true) .map(Ok)
.await
} }
} }
} }
} }
impl CalldataItem { impl CalldataItem {
#[instrument(level = "info", skip_all, err)]
async fn resolve( async fn resolve(
&self, &self,
resolver: &impl ResolverApi, resolver: &impl ResolverApi,
@@ -548,14 +543,7 @@ 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"
)), )),
+26 -35
View File
@@ -15,6 +15,7 @@ use revive_dt_common::{
cached_fs::read_to_string, iterators::FilesWithExtensionIterator, macros::define_wrapper_type, cached_fs::read_to_string, iterators::FilesWithExtensionIterator, macros::define_wrapper_type,
types::Mode, types::Mode,
}; };
use tracing::error;
use crate::{case::Case, mode::ParsedMode}; use crate::{case::Case, mode::ParsedMode};
@@ -24,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()
}
} }
} }
@@ -145,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);
@@ -158,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) {
@@ -177,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
} }
} }
@@ -189,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))
@@ -222,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
} }
} }
@@ -266,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!(
@@ -277,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.
+2 -2
View File
@@ -223,7 +223,7 @@ mod tests {
for (actual, expected) in strings { for (actual, expected) in strings {
let parsed = ParsedMode::from_str(actual) let parsed = ParsedMode::from_str(actual)
.expect(format!("Failed to parse mode string '{actual}'").as_str()); .unwrap_or_else(|_| panic!("Failed to parse mode string '{actual}'"));
assert_eq!( assert_eq!(
expected, expected,
parsed.to_string(), parsed.to_string(),
@@ -249,7 +249,7 @@ mod tests {
for (actual, expected) in strings { for (actual, expected) in strings {
let parsed = ParsedMode::from_str(actual) let parsed = ParsedMode::from_str(actual)
.expect(format!("Failed to parse mode string '{actual}'").as_str()); .unwrap_or_else(|_| panic!("Failed to parse mode string '{actual}'"));
let expected_set: HashSet<_> = expected.into_iter().map(|s| s.to_owned()).collect(); 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(); let actual_set: HashSet<_> = parsed.to_modes().map(|m| m.to_string()).collect();
+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
+3
View File
@@ -29,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
+75 -70
View File
@@ -33,9 +33,12 @@ use alloy::{
}; };
use anyhow::Context; use anyhow::Context;
use revive_common::EVMVersion; use revive_common::EVMVersion;
use tracing::{Instrument, Level}; use tracing::{Instrument, instrument};
use revive_dt_common::{fs::clear_directory, futures::poll}; 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;
@@ -52,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,
@@ -61,8 +65,9 @@ pub struct GethNode {
id: u32, id: u32,
handle: Option<Child>, handle: Option<Child>,
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
@@ -91,7 +96,7 @@ impl GethNode {
const TRACE_POLLING_DURATION: Duration = Duration::from_secs(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(level = "info", 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);
@@ -141,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(level = "info", 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:
@@ -197,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(level = "info", 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();
@@ -231,33 +236,20 @@ impl GethNode {
} }
} }
#[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>,
>,
> + 'static {
let connection_string = self.connection_string();
let wallet = self.wallet.clone();
// 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
// its implementation and therefore it means that when we clone it then it still references
// the same state.
let nonce_manager = self.nonce_manager.clone();
Box::pin(async move {
ProviderBuilder::new() ProviderBuilder::new()
.disable_recommended_fillers() .disable_recommended_fillers()
.filler(FallbackGasFiller::new( .filler(FallbackGasFiller::new(
@@ -265,46 +257,54 @@ impl GethNode {
1_000_000_000, 1_000_000_000,
1_000_000_000, 1_000_000_000,
)) ))
.filler(ChainIdFiller::default()) .filler(self.chain_id_filler.clone())
.filler(NonceFiller::new(nonce_manager)) .filler(NonceFiller::new(self.nonce_manager.clone()))
.wallet(wallet) .wallet(self.wallet.clone())
.connect(&connection_string) .connect(&self.connection_string)
.await .await
.map_err(Into::into) .map_err(Into::into)
})
} }
} }
impl EthereumNode for GethNode { impl EthereumNode for GethNode {
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)] #[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 provider = Arc::new(self.provider().await?); let provider = self.provider().await?;
let transaction_hash = *provider.send_transaction(transaction).await?.tx_hash();
// The following is a fix for the "transaction indexing is in progress" error that we let pending_transaction = provider.send_transaction(transaction).await.inspect_err(
// used to get. You can find more information on this in the following GH issue in geth |err| tracing::error!(%err, "Encountered an error when submitting the transaction"),
)?;
let transaction_hash = *pending_transaction.tx_hash();
// 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);
poll( poll(
Self::RECEIPT_POLLING_DURATION, Self::RECEIPT_POLLING_DURATION,
Default::default(), PollingWaitBehavior::Constant(Duration::from_millis(200)),
move || { move || {
let provider = provider.clone(); let provider = provider.clone();
async move { async move {
@@ -329,7 +329,7 @@ impl EthereumNode for GethNode {
.await .await
} }
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)] #[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,
@@ -338,7 +338,7 @@ impl EthereumNode for GethNode {
let provider = Arc::new(self.provider().await?); let provider = Arc::new(self.provider().await?);
poll( poll(
Self::TRACE_POLLING_DURATION, Self::TRACE_POLLING_DURATION,
Default::default(), PollingWaitBehavior::Constant(Duration::from_millis(200)),
move || { move || {
let provider = provider.clone(); let provider = provider.clone();
let trace_options = trace_options.clone(); let trace_options = trace_options.clone();
@@ -362,7 +362,7 @@ impl EthereumNode for GethNode {
.await .await
} }
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)] #[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),
@@ -379,7 +379,7 @@ impl EthereumNode for GethNode {
} }
} }
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)] #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
async fn balance_of(&self, address: Address) -> anyhow::Result<U256> { async fn balance_of(&self, address: Address) -> anyhow::Result<U256> {
self.provider() self.provider()
.await? .await?
@@ -388,7 +388,7 @@ impl EthereumNode for GethNode {
.map_err(Into::into) .map_err(Into::into)
} }
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)] #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
async fn latest_state_proof( async fn latest_state_proof(
&self, &self,
address: Address, address: Address,
@@ -404,7 +404,7 @@ impl EthereumNode for GethNode {
} }
impl ResolverApi for GethNode { impl ResolverApi for GethNode {
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)] #[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?
@@ -413,7 +413,7 @@ impl ResolverApi for GethNode {
.map_err(Into::into) .map_err(Into::into)
} }
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)] #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
async fn transaction_gas_price(&self, tx_hash: &TxHash) -> anyhow::Result<u128> { async fn transaction_gas_price(&self, tx_hash: &TxHash) -> anyhow::Result<u128> {
self.provider() self.provider()
.await? .await?
@@ -423,7 +423,7 @@ impl ResolverApi for GethNode {
.map(|receipt| receipt.effective_gas_price) .map(|receipt| receipt.effective_gas_price)
} }
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)] #[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?
@@ -433,7 +433,7 @@ impl ResolverApi for GethNode {
.map(|block| block.header.gas_limit as _) .map(|block| block.header.gas_limit as _)
} }
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)] #[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?
@@ -443,7 +443,7 @@ impl ResolverApi for GethNode {
.map(|block| block.header.beneficiary) .map(|block| block.header.beneficiary)
} }
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)] #[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?
@@ -453,7 +453,7 @@ impl ResolverApi for GethNode {
.map(|block| U256::from_be_bytes(block.header.mix_hash.0)) .map(|block| U256::from_be_bytes(block.header.mix_hash.0))
} }
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)] #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
async fn block_base_fee(&self, number: BlockNumberOrTag) -> anyhow::Result<u64> { async fn block_base_fee(&self, number: BlockNumberOrTag) -> anyhow::Result<u64> {
self.provider() self.provider()
.await? .await?
@@ -468,7 +468,7 @@ impl ResolverApi for GethNode {
}) })
} }
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)] #[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?
@@ -478,7 +478,7 @@ impl ResolverApi for GethNode {
.map(|block| block.header.hash) .map(|block| block.header.hash)
} }
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)] #[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?
@@ -488,7 +488,7 @@ impl ResolverApi for GethNode {
.map(|block| block.header.timestamp) .map(|block| block.header.timestamp)
} }
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)] #[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?
@@ -522,20 +522,26 @@ impl Node for GethNode {
id, id,
handle: None, handle: None,
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(level = "info", 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(level = "info", skip_all, fields(geth_node_id = self.id), err)] #[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() {
@@ -557,13 +563,13 @@ impl Node for GethNode {
Ok(()) Ok(())
} }
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)] #[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(level = "info", skip_all, fields(geth_node_id = self.id), err)] #[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")
@@ -576,8 +582,7 @@ impl Node for GethNode {
Ok(String::from_utf8_lossy(&output).into()) Ok(String::from_utf8_lossy(&output).into())
} }
#[tracing::instrument(level = "info", 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"),
@@ -590,7 +595,7 @@ impl Node for GethNode {
} }
impl Drop for GethNode { impl Drop for GethNode {
#[tracing::instrument(level = "info", 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")
} }
+20 -60
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,
}; };
@@ -39,7 +42,6 @@ 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;
@@ -54,12 +56,13 @@ pub struct KitchensinkNode {
substrate_binary: PathBuf, substrate_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,
/// 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
@@ -87,7 +90,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);
@@ -160,7 +162,6 @@ impl KitchensinkNode {
Ok(self) Ok(self)
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
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;
@@ -214,10 +215,6 @@ impl KitchensinkNode {
Self::SUBSTRATE_READY_MARKER, Self::SUBSTRATE_READY_MARKER,
Duration::from_secs(60), Duration::from_secs(60),
) { ) {
tracing::error!(
?error,
"Failed to start substrate, shutting down gracefully"
);
self.shutdown()?; self.shutdown()?;
return Err(error); return Err(error);
}; };
@@ -243,7 +240,6 @@ impl KitchensinkNode {
Self::ETH_PROXY_READY_MARKER, Self::ETH_PROXY_READY_MARKER,
Duration::from_secs(60), Duration::from_secs(60),
) { ) {
tracing::error!(?error, "Failed to start proxy, shutting down gracefully");
self.shutdown()?; self.shutdown()?;
return Err(error); return Err(error);
}; };
@@ -258,7 +254,6 @@ impl KitchensinkNode {
Ok(()) Ok(())
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
fn extract_balance_from_genesis_file( fn extract_balance_from_genesis_file(
&self, &self,
genesis: &Genesis, genesis: &Genesis,
@@ -307,7 +302,6 @@ impl KitchensinkNode {
} }
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
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")
@@ -320,49 +314,33 @@ 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();
let wallet = self.wallet.clone();
// 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
// its implementation and therefore it means that when we clone it then it still references
// the same state.
let nonce_manager = self.nonce_manager.clone();
Box::pin(async move {
ProviderBuilder::new() ProviderBuilder::new()
.disable_recommended_fillers() .disable_recommended_fillers()
.network::<KitchenSinkNetwork>() .network::<KitchenSinkNetwork>()
@@ -371,23 +349,20 @@ impl KitchensinkNode {
1_000_000_000, 1_000_000_000,
1_000_000_000, 1_000_000_000,
)) ))
.filler(ChainIdFiller::default()) .filler(self.chain_id_filler.clone())
.filler(NonceFiller::new(nonce_manager)) .filler(NonceFiller::new(self.nonce_manager.clone()))
.wallet(wallet) .wallet(self.wallet.clone())
.connect(&connection_string) .connect(&self.rpc_url)
.await .await
.map_err(Into::into) .map_err(Into::into)
})
} }
} }
impl EthereumNode for KitchensinkNode { impl EthereumNode for KitchensinkNode {
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
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?
@@ -395,11 +370,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), err)]
async fn trace_transaction( async fn trace_transaction(
&self, &self,
transaction: &TransactionReceipt, transaction: &TransactionReceipt,
@@ -413,7 +386,6 @@ impl EthereumNode for KitchensinkNode {
.await?) .await?)
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
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),
@@ -430,7 +402,6 @@ impl EthereumNode for KitchensinkNode {
} }
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
async fn balance_of(&self, address: Address) -> anyhow::Result<U256> { async fn balance_of(&self, address: Address) -> anyhow::Result<U256> {
self.provider() self.provider()
.await? .await?
@@ -439,7 +410,6 @@ impl EthereumNode for KitchensinkNode {
.map_err(Into::into) .map_err(Into::into)
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
async fn latest_state_proof( async fn latest_state_proof(
&self, &self,
address: Address, address: Address,
@@ -455,7 +425,6 @@ impl EthereumNode for KitchensinkNode {
} }
impl ResolverApi for KitchensinkNode { impl ResolverApi for KitchensinkNode {
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
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?
@@ -464,7 +433,6 @@ impl ResolverApi for KitchensinkNode {
.map_err(Into::into) .map_err(Into::into)
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
async fn transaction_gas_price(&self, tx_hash: &TxHash) -> anyhow::Result<u128> { async fn transaction_gas_price(&self, tx_hash: &TxHash) -> anyhow::Result<u128> {
self.provider() self.provider()
.await? .await?
@@ -474,7 +442,6 @@ impl ResolverApi for KitchensinkNode {
.map(|receipt| receipt.effective_gas_price) .map(|receipt| receipt.effective_gas_price)
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
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?
@@ -484,7 +451,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), err)]
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?
@@ -494,7 +460,6 @@ impl ResolverApi for KitchensinkNode {
.map(|block| block.header.beneficiary) .map(|block| block.header.beneficiary)
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
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?
@@ -504,7 +469,6 @@ impl ResolverApi for KitchensinkNode {
.map(|block| U256::from_be_bytes(block.header.mix_hash.0)) .map(|block| U256::from_be_bytes(block.header.mix_hash.0))
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
async fn block_base_fee(&self, number: BlockNumberOrTag) -> anyhow::Result<u64> { async fn block_base_fee(&self, number: BlockNumberOrTag) -> anyhow::Result<u64> {
self.provider() self.provider()
.await? .await?
@@ -519,7 +483,6 @@ impl ResolverApi for KitchensinkNode {
}) })
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
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?
@@ -529,7 +492,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), err)]
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?
@@ -539,7 +501,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), err)]
async fn last_block_number(&self) -> anyhow::Result<BlockNumber> { async fn last_block_number(&self) -> anyhow::Result<BlockNumber> {
self.provider() self.provider()
.await? .await?
@@ -570,11 +531,12 @@ impl Node for KitchensinkNode {
substrate_binary: config.kitchensink.clone(), substrate_binary: config.kitchensink.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(),
// 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.
@@ -582,12 +544,14 @@ impl Node for KitchensinkNode {
} }
} }
#[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), err)]
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() {
@@ -614,12 +578,10 @@ impl Node for KitchensinkNode {
Ok(()) Ok(())
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
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), err)]
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")
@@ -632,8 +594,7 @@ 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"),
@@ -646,7 +607,6 @@ impl Node for KitchensinkNode {
} }
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")
} }
+4 -1
View File
@@ -18,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.
@@ -36,7 +39,7 @@ 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. /// Returns the EVM version of the node.
fn evm_version() -> EVMVersion; fn evm_version() -> EVMVersion;
-1
View File
@@ -63,7 +63,6 @@ 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());
node.spawn(genesis)?; node.spawn(genesis)?;
Ok(node) Ok(node)
} }
+3 -1
View File
@@ -14,6 +14,8 @@ revive-dt-format = { workspace = true }
revive-dt-compiler = { workspace = true } revive-dt-compiler = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
tracing = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
[lints]
workspace = true
-2
View File
@@ -185,8 +185,6 @@ impl Report {
let file = File::create(&path).context(path.display().to_string())?; let file = File::create(&path).context(path.display().to_string())?;
serde_json::to_writer_pretty(file, &self)?; serde_json::to_writer_pretty(file, &self)?;
tracing::info!("report written to: {}", path.display());
Ok(()) Ok(())
} }
} }
+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
View File
@@ -39,10 +39,7 @@ pub(crate) async fn get_or_download(
} }
async fn download_to_file(path: &Path, downloader: &SolcDownloader) -> 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(());
}; };
-1
View File
@@ -107,7 +107,6 @@ impl SolcDownloader {
/// 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 builds = List::download(self.list).await?.builds;
let build = builds let build = builds
.iter() .iter()