Compare commits

...

15 Commits

Author SHA1 Message Date
Omar Abdulla 565b3803b7 Merge remote-tracking branch 'origin/main' into feature/output-formats 2025-10-09 16:12:08 +03:00
Omar Abdulla b5de071594 Merge branch 'main' into feature/output-formats 2025-10-09 16:11:53 +03:00
Omar 21e25f09e6 Zombienet & Benchmarks Cleanups (#184)
* Minor zombienet cleanups

* Remove un-necessary trace call from the benchmark driver

* Improve the benchmarks driver

* Ignore the lighthouse tests

* Allow for the consensus to be specified for the revive dev node

* Ignore the zombienet tests for the time being
2025-10-09 11:41:01 +00:00
Omar Abdulla 18ee331529 Add the mode to the output 2025-10-09 01:07:19 +03:00
Omar Abdulla 37a689a73e Add different output formats 2025-10-09 01:03:20 +03:00
Marios 8c412dc924 Use absolute path for zombienet to avoid conflicts (#178) 2025-10-07 16:03:18 +00:00
Omar 6da3172581 Fix/fallback gas for substrate (#177)
* Update compiler semaphore

* Fix the fallback gas for substrate chains

* Fix the concurrency & the substrate gas limit fallback value
2025-10-07 02:25:37 +00:00
Marios c6eb04b04e Support zombienet (#172)
* Basic zombie node definition

* [WIP] - Impl EthereumNode for zombie node

* Remove unused imports

* [WIP] Support substrate node in zombienet network

* Impl zombie node resolver && node for zombie node

* Spawn eth-rpc on top of collator node

* Implement ZombienetPlatform and integrate zombie node with eth rpc

* Add wourkaround to run tests

* Add few comments

* fmt

* Replace default transaction request in test

* Merge - Fix conficts with main

* fmt

* Clippy fix

* Add polkadot and parachain node to ci

* CI - Fetch polkadot binaries from releases

* Fix unit test assertion

* Minor doc improvements

* Change names from ZombieNet to Zombienet and switch from Command to Process

* Refactor ZombieNode to cache provider using OnceCell

* CI: Cache polkadot binaries and use them if available

* Fix conficts with main

* Refactor shared_node to return static reference and add shared_state for context access

* fmt

* Rename ZombienetConfiguration to PolkadotParachainConfiguration and update related usage
2025-10-07 02:24:20 +00:00
Omar e5114d31dc Update compiler semaphore (#176) 2025-10-05 22:28:57 +00:00
Omar 74fdeb4a2e Core Benchmarking Infra (#175)
* Implement a solution for the pre-fund account limit

* Update the account pre-funding handling

* Fix the lighthouse node tracing issue

* refactor existing dt infra

* Implement the platform driver

* Wire up the cleaned up driver implementation

* Implement the core benchmarking components

* Remove some debug logging

* Fix issues in the benchmarks driver

* Implement a global concurrency limit on provider requests

* Update the concurrency limit

* Update the concurrency limit

* Cleanups

* Update the lighthouse ports

* Ignore certain tests

* Update the new geth test
2025-10-05 15:09:01 +00:00
Omar f9dc362c03 Lighthouse Node (#173)
* Add a lighthouse node implementation

* Implement production geth using kurtosis

* Connect the lighthouse node with the platforms

* Update the ci to include cargo fmt

* Add rustfmt to ci

* Add formatting component for macos

* Fix CI

* Add the cargo clippy component

* Install kurtosis in cli

* fix ci

* Skip lighthouse tests in MacOS in CI

* Increase the wait duration of kurtosis
2025-09-28 12:44:19 +00:00
Omar c2ba2cfed6 Cleanup Processes (#171)
* Clean up the process flow for nodes

* Cleanup some of the node tests to use shared nodes

* Commit dev-genesis
2025-09-24 02:47:36 +00:00
Omar 3dda739cef Add step paths (#163)
* Support repetitions in the tool

* Add support for account allocations

* Update the JSON schema

* Support virtual repeats

* Add a step path

* Update the schema
2025-09-22 03:53:16 +00:00
Omar 97e3f8bbff Support virtual repeats for benchmarks (#162)
* Support repetitions in the tool

* Add support for account allocations

* Update the JSON schema

* Support virtual repeats
2025-09-22 03:35:31 +00:00
Omar 7189361a58 Allow account allocations (#161)
* Support repetitions in the tool

* Add support for account allocations

* Update the JSON schema
2025-09-22 03:19:55 +00:00
62 changed files with 9906 additions and 2268 deletions
+94 -4
View File
@@ -15,6 +15,7 @@ concurrency:
env:
CARGO_TERM_COLOR: always
POLKADOT_VERSION: polkadot-stable2506-2
jobs:
cache-polkadot:
@@ -65,6 +66,37 @@ jobs:
run: |
cd polkadot-sdk
cargo install --path substrate/frame/revive/rpc --bin eth-rpc
- name: Cache downloaded Polkadot binaries
id: cache-polkadot
uses: actions/cache@v3
with:
path: |
~/polkadot-cache/polkadot
~/polkadot-cache/polkadot-execute-worker
~/polkadot-cache/polkadot-prepare-worker
~/polkadot-cache/polkadot-parachain
key: polkadot-downloaded-${{ matrix.os }}-${{ env.POLKADOT_VERSION }}
- name: Download Polkadot binaries on macOS
if: matrix.os == 'macos-14' && steps.cache-polkadot.outputs.cache-hit != 'true'
run: |
mkdir -p ~/polkadot-cache
curl -sL https://github.com/paritytech/polkadot-sdk/releases/download/${{ env.POLKADOT_VERSION }}/polkadot-aarch64-apple-darwin -o ~/polkadot-cache/polkadot
curl -sL https://github.com/paritytech/polkadot-sdk/releases/download/${{ env.POLKADOT_VERSION }}/polkadot-execute-worker-aarch64-apple-darwin -o ~/polkadot-cache/polkadot-execute-worker
curl -sL https://github.com/paritytech/polkadot-sdk/releases/download/${{ env.POLKADOT_VERSION }}/polkadot-prepare-worker-aarch64-apple-darwin -o ~/polkadot-cache/polkadot-prepare-worker
curl -sL https://github.com/paritytech/polkadot-sdk/releases/download/${{ env.POLKADOT_VERSION }}/polkadot-parachain-aarch64-apple-darwin -o ~/polkadot-cache/polkadot-parachain
chmod +x ~/polkadot-cache/*
- name: Download Polkadot binaries on Ubuntu
if: matrix.os == 'ubuntu-24.04' && steps.cache-polkadot.outputs.cache-hit != 'true'
run: |
mkdir -p ~/polkadot-cache
curl -sL https://github.com/paritytech/polkadot-sdk/releases/download/${{ env.POLKADOT_VERSION }}/polkadot -o ~/polkadot-cache/polkadot
curl -sL https://github.com/paritytech/polkadot-sdk/releases/download/${{ env.POLKADOT_VERSION }}/polkadot-execute-worker -o ~/polkadot-cache/polkadot-execute-worker
curl -sL https://github.com/paritytech/polkadot-sdk/releases/download/${{ env.POLKADOT_VERSION }}/polkadot-prepare-worker -o ~/polkadot-cache/polkadot-prepare-worker
curl -sL https://github.com/paritytech/polkadot-sdk/releases/download/${{ env.POLKADOT_VERSION }}/polkadot-parachain -o ~/polkadot-cache/polkadot-parachain
chmod +x ~/polkadot-cache/*
ci:
name: CI on ${{ matrix.os }}
@@ -86,15 +118,33 @@ jobs:
~/.cargo/bin/eth-rpc
key: polkadot-binaries-${{ matrix.os }}-${{ hashFiles('polkadot-sdk/.git') }}
- name: Restore downloaded Polkadot binaries from cache
uses: actions/cache@v3
with:
path: |
~/polkadot-cache/polkadot
~/polkadot-cache/polkadot-execute-worker
~/polkadot-cache/polkadot-prepare-worker
~/polkadot-cache/polkadot-parachain
key: polkadot-downloaded-${{ matrix.os }}-${{ env.POLKADOT_VERSION }}
- name: Install Polkadot binaries
run: |
sudo cp ~/polkadot-cache/polkadot /usr/local/bin/
sudo cp ~/polkadot-cache/polkadot-execute-worker /usr/local/bin/
sudo cp ~/polkadot-cache/polkadot-prepare-worker /usr/local/bin/
sudo cp ~/polkadot-cache/polkadot-parachain /usr/local/bin/
sudo chmod +x /usr/local/bin/polkadot*
- name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
rustflags: ""
- name: Add wasm32 target
- name: Add wasm32 target and formatting
run: |
rustup target add wasm32-unknown-unknown
rustup component add rust-src
rustup component add rust-src rustfmt clippy
- name: Install Geth on Ubuntu
if: matrix.os == 'ubuntu-24.04'
@@ -141,6 +191,17 @@ jobs:
chmod +x resolc
sudo mv resolc /usr/local/bin
- name: Install Kurtosis on macOS
if: matrix.os == 'macos-14'
run: brew install kurtosis-tech/tap/kurtosis-cli
- name: Install Kurtosis on Ubuntu
if: matrix.os == 'ubuntu-24.04'
run: |
echo "deb [trusted=yes] https://apt.fury.io/kurtosis-tech/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list
sudo apt update
sudo apt install kurtosis-cli
- name: Machete
uses: bnjbvr/cargo-machete@v0.7.1
@@ -159,5 +220,34 @@ jobs:
- name: Check resolc version
run: resolc --version
- name: Test cargo workspace
run: make test
- name: Check polkadot version
run: polkadot --version
- name: Check polkadot-parachain version
run: polkadot-parachain --version
- name: Check polkadot-execute-worker version
run: polkadot-execute-worker --version
- name: Check polkadot-prepare-worker version
run: polkadot-prepare-worker --version
- name: Test Formatting
run: make format
- name: Test Clippy
run: make clippy
- name: Test Machete
run: make machete
- name: Unit Tests
if: matrix.os == 'ubuntu-24.04'
run: cargo test --workspace -- --nocapture
# We can't install docker in the MacOS image used in CI and therefore we need to skip the
# Kurtosis and lighthouse related tests when running the CI on MacOS.
- name: Unit Tests
if: matrix.os == 'macos-14'
run: |
cargo test --workspace -- --nocapture --skip lighthouse_geth::tests::
+2 -1
View File
@@ -12,4 +12,5 @@ profile.json.gz
resolc-compiler-tests
workdir
!/schema.json
!/schema.json
!/dev-genesis.json
Generated
+2403 -112
View File
File diff suppressed because it is too large Load Diff
+11 -4
View File
@@ -22,9 +22,9 @@ revive-dt-node-pool = { version = "0.1.0", path = "crates/node-pool" }
revive-dt-report = { version = "0.1.0", path = "crates/report" }
revive-dt-solc-binaries = { version = "0.1.0", path = "crates/solc-binaries" }
alloy-primitives = "1.2.1"
alloy-sol-types = "1.2.1"
ansi_term = "0.12.1"
anyhow = "1.0"
async-stream = { version = "0.3.6" }
bson = { version = "2.15.0" }
cacache = { version = "13.1.0" }
clap = { version = "4", features = ["derive"] }
@@ -45,7 +45,8 @@ serde_json = { version = "1.0", default-features = false, features = [
"std",
"unbounded_depth",
] }
serde_with = { version = "3.14.0" }
serde_with = { version = "3.14.0", features = ["hex"] }
serde_yaml_ng = { version = "0.10.0" }
sha2 = { version = "0.10.9" }
sp-core = "36.1.0"
sp-runtime = "41.1.0"
@@ -58,6 +59,7 @@ tokio = { version = "1.47.0", default-features = false, features = [
"process",
"rt",
] }
tower = { version = "0.5.2", features = ["limit"] }
uuid = { version = "1.8", features = ["v4"] }
tracing = { version = "0.1.41" }
tracing-appender = { version = "0.2.3" }
@@ -73,13 +75,17 @@ revive-solc-json-interface = { git = "https://github.com/paritytech/revive", rev
revive-common = { git = "https://github.com/paritytech/revive", rev = "3389865af7c3ff6f29a586d82157e8bc573c1a8e" }
revive-differential = { git = "https://github.com/paritytech/revive", rev = "3389865af7c3ff6f29a586d82157e8bc573c1a8e" }
zombienet-sdk = { git = "https://github.com/paritytech/zombienet-sdk.git", rev = "891f6554354ce466abd496366dbf8b4f82141241" }
[workspace.dependencies.alloy]
version = "1.0.22"
version = "1.0.37"
default-features = false
features = [
"json-abi",
"providers",
"provider-ws",
"provider-ipc",
"provider-http",
"provider-debug-api",
"reqwest",
"rpc-types",
@@ -89,6 +95,7 @@ features = [
"serde",
"rpc-types-eth",
"genesis",
"sol-types",
]
[profile.bench]
+1
View File
@@ -44,6 +44,7 @@ This section describes the required dependencies that this framework requires to
- 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.
- Kurtosis - The Kurtosis CLI tool is required for the production Ethereum mainnet-like node configuration with Geth as the execution layer and lighthouse as the consensus layer. Kurtosis also requires docker to be installed since it runs everything inside of docker containers.
All of the above need to be installed and available in the path in order for the tool to work.
+1
View File
@@ -9,6 +9,7 @@ repository.workspace = true
rust-version.workspace = true
[dependencies]
alloy = { workspace = true }
anyhow = { workspace = true }
clap = { workspace = true }
moka = { workspace = true, features = ["sync"] }
+10
View File
@@ -29,6 +29,8 @@ use strum::{AsRefStr, Display, EnumString, IntoStaticStr};
pub enum PlatformIdentifier {
/// The Go-ethereum reference full node EVM implementation with the solc compiler.
GethEvmSolc,
/// The Lighthouse Go-ethereum reference full node EVM implementation with the solc compiler.
LighthouseGethEvmSolc,
/// The kitchensink node with the PolkaVM backend with the resolc compiler.
KitchensinkPolkavmResolc,
/// The kitchensink node with the REVM backend with the solc compiler.
@@ -37,6 +39,10 @@ pub enum PlatformIdentifier {
ReviveDevNodePolkavmResolc,
/// The revive dev node with the REVM backend with the solc compiler.
ReviveDevNodeRevmSolc,
/// A zombienet based Substrate/Polkadot node with the PolkaVM backend with the resolc compiler.
ZombienetPolkavmResolc,
/// A zombienet based Substrate/Polkadot node with the REVM backend with the solc compiler.
ZombienetRevmSolc,
}
/// An enum of the platform identifiers of all of the platforms supported by this framework.
@@ -87,10 +93,14 @@ pub enum CompilerIdentifier {
pub enum NodeIdentifier {
/// The go-ethereum node implementation.
Geth,
/// The go-ethereum node implementation.
LighthouseGeth,
/// The Kitchensink node implementation.
Kitchensink,
/// The revive dev node implementation.
ReviveDevNode,
/// A zombienet spawned nodes
Zombienet,
}
/// An enum representing the identifiers of the supported VMs.
+4
View File
@@ -1,7 +1,11 @@
mod identifiers;
mod mode;
mod private_key_allocator;
mod round_robin_pool;
mod version_or_requirement;
pub use identifiers::*;
pub use mode::*;
pub use private_key_allocator::*;
pub use round_robin_pool::*;
pub use version_or_requirement::*;
@@ -0,0 +1,36 @@
use alloy::primitives::U256;
use alloy::signers::local::PrivateKeySigner;
use anyhow::{Context, Result, bail};
/// This is a sequential private key allocator. When instantiated, it allocated private keys in
/// sequentially and in order until the maximum private key specified is reached.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PrivateKeyAllocator {
/// The next private key to be returned by the allocator when requested.
next_private_key: U256,
/// The highest private key (exclusive) that can be returned by this allocator.
highest_private_key_inclusive: U256,
}
impl PrivateKeyAllocator {
/// Creates a new instance of the private key allocator.
pub fn new(highest_private_key_inclusive: U256) -> Self {
Self {
next_private_key: U256::ONE,
highest_private_key_inclusive,
}
}
/// Allocates a new private key and errors out if the maximum private key has been reached.
pub fn allocate(&mut self) -> Result<PrivateKeySigner> {
if self.next_private_key > self.highest_private_key_inclusive {
bail!("Attempted to allocate a private key but failed since all have been allocated");
};
let private_key =
PrivateKeySigner::from_slice(self.next_private_key.to_be_bytes::<32>().as_slice())
.context("Failed to convert the private key digits into a private key")?;
self.next_private_key += U256::ONE;
Ok(private_key)
}
}
@@ -0,0 +1,24 @@
use std::sync::atomic::{AtomicUsize, Ordering};
pub struct RoundRobinPool<T> {
next_index: AtomicUsize,
items: Vec<T>,
}
impl<T> RoundRobinPool<T> {
pub fn new(items: Vec<T>) -> Self {
Self {
next_index: Default::default(),
items,
}
}
pub fn round_robin(&self) -> &T {
let current = self.next_index.fetch_add(1, Ordering::SeqCst) % self.items.len();
self.items.get(current).unwrap()
}
pub fn iter(&self) -> impl Iterator<Item = &T> {
self.items.iter()
}
}
-1
View File
@@ -16,7 +16,6 @@ revive-dt-solc-binaries = { workspace = true }
revive-common = { workspace = true }
alloy = { workspace = true }
alloy-primitives = { workspace = true }
anyhow = { workspace = true }
dashmap = { workspace = true }
foundry-compilers-artifacts = { workspace = true }
+1 -1
View File
@@ -11,7 +11,7 @@ use std::{
};
use alloy::json_abi::JsonAbi;
use alloy_primitives::Address;
use alloy::primitives::Address;
use anyhow::{Context as _, Result};
use semver::Version;
use serde::{Deserialize, Serialize};
+14
View File
@@ -16,6 +16,7 @@ use revive_solc_json_interface::{
SolcStandardJsonInputSettingsOptimizer, SolcStandardJsonInputSettingsSelection,
SolcStandardJsonOutput,
};
use tracing::{Span, field::display};
use crate::{
CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler, solc::Solc,
@@ -80,6 +81,16 @@ impl SolidityCompiler for Resolc {
}
#[tracing::instrument(level = "debug", ret)]
#[tracing::instrument(
level = "error",
skip_all,
fields(
resolc_version = %self.version(),
solc_version = %self.0.solc.version(),
json_in = tracing::field::Empty
),
err(Debug)
)]
fn build(
&self,
CompilerInput {
@@ -141,6 +152,7 @@ impl SolidityCompiler for Resolc {
polkavm: None,
},
};
Span::current().record("json_in", display(serde_json::to_string(&input).unwrap()));
let path = &self.0.resolc_path;
let mut command = AsyncCommand::new(path);
@@ -148,6 +160,8 @@ impl SolidityCompiler for Resolc {
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.arg("--solc")
.arg(self.0.solc.path())
.arg("--standard-json");
if let Some(ref base_path) = base_path {
+19 -9
View File
@@ -10,8 +10,9 @@ use std::{
use dashmap::DashMap;
use revive_dt_common::types::VersionOrRequirement;
use revive_dt_config::{ResolcConfiguration, SolcConfiguration, WorkingDirectoryConfiguration};
use revive_dt_config::{SolcConfiguration, WorkingDirectoryConfiguration};
use revive_dt_solc_binaries::download_solc;
use tracing::{Span, field::display, info};
use crate::{CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler};
@@ -39,9 +40,7 @@ struct SolcInner {
impl Solc {
pub async fn new(
context: impl AsRef<SolcConfiguration>
+ AsRef<ResolcConfiguration>
+ AsRef<WorkingDirectoryConfiguration>,
context: impl AsRef<SolcConfiguration> + AsRef<WorkingDirectoryConfiguration>,
version: impl Into<Option<VersionOrRequirement>>,
) -> Result<Self> {
// This is a cache for the compiler objects so that whenever the same compiler version is
@@ -69,6 +68,11 @@ impl Solc {
Ok(COMPILERS_CACHE
.entry((path.clone(), version.clone()))
.or_insert_with(|| {
info!(
solc_path = %path.display(),
solc_version = %version,
"Created a new solc compiler object"
);
Self(Arc::new(SolcInner {
solc_path: path,
solc_version: version,
@@ -88,6 +92,12 @@ impl SolidityCompiler for Solc {
}
#[tracing::instrument(level = "debug", ret)]
#[tracing::instrument(
level = "error",
skip_all,
fields(json_in = tracing::field::Empty),
err(Debug)
)]
fn build(
&self,
CompilerInput {
@@ -166,12 +176,14 @@ impl SolidityCompiler for Solc {
},
};
Span::current().record("json_in", display(serde_json::to_string(&input).unwrap()));
let path = &self.0.solc_path;
let mut command = AsyncCommand::new(path);
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stderr(Stdio::null())
.arg("--standard-json");
if let Some(ref base_path) = base_path {
@@ -205,20 +217,18 @@ impl SolidityCompiler for Solc {
if !output.status.success() {
let json_in = serde_json::to_string_pretty(&input)
.context("Failed to pretty-print Standard JSON input for logging")?;
let message = String::from_utf8_lossy(&output.stderr);
tracing::error!(
status = %output.status,
message = %message,
json_input = json_in,
"Compilation using solc failed"
);
anyhow::bail!("Compilation failed with an error: {message}");
anyhow::bail!("Compilation failed");
}
let parsed = serde_json::from_slice::<SolcOutput>(&output.stdout)
.map_err(|e| {
anyhow::anyhow!(
"failed to parse resolc JSON output: {e}\nstderr: {}",
"failed to parse resolc JSON output: {e}\nstdout: {}",
String::from_utf8_lossy(&output.stdout)
)
})
@@ -7,7 +7,10 @@ pragma solidity >=0.6.9;
import "./callable.sol";
contract Main {
function main(uint[1] calldata p1, Callable callable) public returns(uint) {
function main(
uint[1] calldata p1,
Callable callable
) public pure returns (uint) {
return callable.f(p1);
}
}
+368 -35
View File
@@ -28,7 +28,11 @@ use temp_dir::TempDir;
#[command(name = "retester")]
pub enum Context {
/// Executes tests in the MatterLabs format differentially on multiple targets concurrently.
ExecuteTests(Box<TestExecutionContext>),
Test(Box<TestExecutionContext>),
/// Executes differential benchmarks on various platforms.
Benchmark(Box<BenchmarkingContext>),
/// Exports the JSON schema of the MatterLabs test format used by the tool.
ExportJsonSchema,
}
@@ -46,7 +50,18 @@ impl Context {
impl AsRef<WorkingDirectoryConfiguration> for Context {
fn as_ref(&self) -> &WorkingDirectoryConfiguration {
match self {
Self::ExecuteTests(context) => context.as_ref().as_ref(),
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(),
}
}
}
impl AsRef<CorpusConfiguration> for Context {
fn as_ref(&self) -> &CorpusConfiguration {
match self {
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(),
}
}
@@ -55,7 +70,8 @@ impl AsRef<WorkingDirectoryConfiguration> for Context {
impl AsRef<SolcConfiguration> for Context {
fn as_ref(&self) -> &SolcConfiguration {
match self {
Self::ExecuteTests(context) => context.as_ref().as_ref(),
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(),
}
}
@@ -64,7 +80,8 @@ impl AsRef<SolcConfiguration> for Context {
impl AsRef<ResolcConfiguration> for Context {
fn as_ref(&self) -> &ResolcConfiguration {
match self {
Self::ExecuteTests(context) => context.as_ref().as_ref(),
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(),
}
}
@@ -73,7 +90,28 @@ impl AsRef<ResolcConfiguration> for Context {
impl AsRef<GethConfiguration> for Context {
fn as_ref(&self) -> &GethConfiguration {
match self {
Self::ExecuteTests(context) => context.as_ref().as_ref(),
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(),
}
}
}
impl AsRef<KurtosisConfiguration> for Context {
fn as_ref(&self) -> &KurtosisConfiguration {
match self {
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(),
}
}
}
impl AsRef<PolkadotParachainConfiguration> for Context {
fn as_ref(&self) -> &PolkadotParachainConfiguration {
match self {
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(),
}
}
@@ -82,7 +120,8 @@ impl AsRef<GethConfiguration> for Context {
impl AsRef<KitchensinkConfiguration> for Context {
fn as_ref(&self) -> &KitchensinkConfiguration {
match self {
Self::ExecuteTests(context) => context.as_ref().as_ref(),
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(),
}
}
@@ -91,7 +130,8 @@ impl AsRef<KitchensinkConfiguration> for Context {
impl AsRef<ReviveDevNodeConfiguration> for Context {
fn as_ref(&self) -> &ReviveDevNodeConfiguration {
match self {
Self::ExecuteTests(context) => context.as_ref().as_ref(),
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(),
}
}
@@ -100,7 +140,8 @@ impl AsRef<ReviveDevNodeConfiguration> for Context {
impl AsRef<EthRpcConfiguration> for Context {
fn as_ref(&self) -> &EthRpcConfiguration {
match self {
Self::ExecuteTests(context) => context.as_ref().as_ref(),
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(),
}
}
@@ -109,7 +150,11 @@ impl AsRef<EthRpcConfiguration> for Context {
impl AsRef<GenesisConfiguration> for Context {
fn as_ref(&self) -> &GenesisConfiguration {
match self {
Self::ExecuteTests(context) => context.as_ref().as_ref(),
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(..) => {
static GENESIS: LazyLock<GenesisConfiguration> = LazyLock::new(Default::default);
&GENESIS
}
Self::ExportJsonSchema => unreachable!(),
}
}
@@ -118,7 +163,8 @@ impl AsRef<GenesisConfiguration> for Context {
impl AsRef<WalletConfiguration> for Context {
fn as_ref(&self) -> &WalletConfiguration {
match self {
Self::ExecuteTests(context) => context.as_ref().as_ref(),
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(),
}
}
@@ -127,7 +173,8 @@ impl AsRef<WalletConfiguration> for Context {
impl AsRef<ConcurrencyConfiguration> for Context {
fn as_ref(&self) -> &ConcurrencyConfiguration {
match self {
Self::ExecuteTests(context) => context.as_ref().as_ref(),
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(),
}
}
@@ -136,7 +183,8 @@ impl AsRef<ConcurrencyConfiguration> for Context {
impl AsRef<CompilationConfiguration> for Context {
fn as_ref(&self) -> &CompilationConfiguration {
match self {
Self::ExecuteTests(context) => context.as_ref().as_ref(),
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(),
}
}
@@ -145,7 +193,8 @@ impl AsRef<CompilationConfiguration> for Context {
impl AsRef<ReportConfiguration> for Context {
fn as_ref(&self) -> &ReportConfiguration {
match self {
Self::ExecuteTests(context) => context.as_ref().as_ref(),
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(),
}
}
@@ -153,6 +202,18 @@ impl AsRef<ReportConfiguration> for Context {
#[derive(Clone, Debug, Parser, Serialize)]
pub struct TestExecutionContext {
/// The set of platforms that the differential tests should run on.
#[arg(
short = 'p',
long = "platform",
default_values = ["geth-evm-solc", "revive-dev-node-polkavm-resolc"]
)]
pub platforms: Vec<PlatformIdentifier>,
/// The output format to use for the tool's output.
#[arg(short, long, default_value_t = OutputFormat::CargoTestLike)]
pub output_format: OutputFormat,
/// The working directory that the program will use for all of the temporary artifacts needed at
/// runtime.
///
@@ -166,17 +227,9 @@ pub struct TestExecutionContext {
)]
pub working_directory: WorkingDirectoryConfiguration,
/// The set of platforms that the differential tests should run on.
#[arg(
short = 'p',
long = "platform",
default_values = ["geth-evm-solc", "revive-dev-node-polkavm-resolc"]
)]
pub platforms: Vec<PlatformIdentifier>,
/// A list of test corpus JSON files to be tested.
#[arg(long = "corpus", short)]
pub corpus: Vec<PathBuf>,
/// Configuration parameters for the corpus files to use.
#[clap(flatten, next_help_heading = "Corpus Configuration")]
pub corpus_configuration: CorpusConfiguration,
/// Configuration parameters for the solc compiler.
#[clap(flatten, next_help_heading = "Solc Configuration")]
@@ -186,10 +239,18 @@ pub struct TestExecutionContext {
#[clap(flatten, next_help_heading = "Resolc Configuration")]
pub resolc_configuration: ResolcConfiguration,
/// Configuration parameters for the Polkadot Parachain.
#[clap(flatten, next_help_heading = "Polkadot Parachain Configuration")]
pub polkadot_parachain_configuration: PolkadotParachainConfiguration,
/// Configuration parameters for the geth node.
#[clap(flatten, next_help_heading = "Geth Configuration")]
pub geth_configuration: GethConfiguration,
/// Configuration parameters for the lighthouse node.
#[clap(flatten, next_help_heading = "Lighthouse Configuration")]
pub lighthouse_configuration: KurtosisConfiguration,
/// Configuration parameters for the Kitchensink.
#[clap(flatten, next_help_heading = "Kitchensink Configuration")]
pub kitchensink_configuration: KitchensinkConfiguration,
@@ -223,6 +284,87 @@ pub struct TestExecutionContext {
pub report_configuration: ReportConfiguration,
}
#[derive(Clone, Debug, Parser, Serialize)]
pub struct BenchmarkingContext {
/// The working directory that the program will use for all of the temporary artifacts needed at
/// runtime.
///
/// If not specified, then a temporary directory will be created and used by the program for all
/// temporary artifacts.
#[clap(
short,
long,
default_value = "",
value_hint = ValueHint::DirPath,
)]
pub working_directory: WorkingDirectoryConfiguration,
/// The set of platforms that the differential tests should run on.
#[arg(
short = 'p',
long = "platform",
default_values = ["geth-evm-solc", "revive-dev-node-polkavm-resolc"]
)]
pub platforms: Vec<PlatformIdentifier>,
/// The default repetition count for any workload specified but that doesn't contain a repeat
/// step.
#[arg(short = 'r', long = "default-repetition-count", default_value_t = 1000)]
pub default_repetition_count: usize,
/// Configuration parameters for the corpus files to use.
#[clap(flatten, next_help_heading = "Corpus Configuration")]
pub corpus_configuration: CorpusConfiguration,
/// Configuration parameters for the solc compiler.
#[clap(flatten, next_help_heading = "Solc Configuration")]
pub solc_configuration: SolcConfiguration,
/// Configuration parameters for the resolc compiler.
#[clap(flatten, next_help_heading = "Resolc Configuration")]
pub resolc_configuration: ResolcConfiguration,
/// Configuration parameters for the geth node.
#[clap(flatten, next_help_heading = "Geth Configuration")]
pub geth_configuration: GethConfiguration,
/// Configuration parameters for the lighthouse node.
#[clap(flatten, next_help_heading = "Lighthouse Configuration")]
pub lighthouse_configuration: KurtosisConfiguration,
/// Configuration parameters for the Kitchensink.
#[clap(flatten, next_help_heading = "Kitchensink Configuration")]
pub kitchensink_configuration: KitchensinkConfiguration,
/// Configuration parameters for the Polkadot Parachain.
#[clap(flatten, next_help_heading = "Polkadot Parachain Configuration")]
pub polkadot_parachain_configuration: PolkadotParachainConfiguration,
/// Configuration parameters for the Revive Dev Node.
#[clap(flatten, next_help_heading = "Revive Dev Node Configuration")]
pub revive_dev_node_configuration: ReviveDevNodeConfiguration,
/// Configuration parameters for the Eth Rpc.
#[clap(flatten, next_help_heading = "Eth RPC Configuration")]
pub eth_rpc_configuration: EthRpcConfiguration,
/// Configuration parameters for the wallet.
#[clap(flatten, next_help_heading = "Wallet Configuration")]
pub wallet_configuration: WalletConfiguration,
/// Configuration parameters for concurrency.
#[clap(flatten, next_help_heading = "Concurrency Configuration")]
pub concurrency_configuration: ConcurrencyConfiguration,
/// Configuration parameters for the compilers and compilation.
#[clap(flatten, next_help_heading = "Compilation Configuration")]
pub compilation_configuration: CompilationConfiguration,
/// Configuration parameters for the report.
#[clap(flatten, next_help_heading = "Report Configuration")]
pub report_configuration: ReportConfiguration,
}
impl Default for TestExecutionContext {
fn default() -> Self {
Self::parse_from(["execution-context"])
@@ -235,6 +377,12 @@ impl AsRef<WorkingDirectoryConfiguration> for TestExecutionContext {
}
}
impl AsRef<CorpusConfiguration> for TestExecutionContext {
fn as_ref(&self) -> &CorpusConfiguration {
&self.corpus_configuration
}
}
impl AsRef<SolcConfiguration> for TestExecutionContext {
fn as_ref(&self) -> &SolcConfiguration {
&self.solc_configuration
@@ -253,6 +401,18 @@ impl AsRef<GethConfiguration> for TestExecutionContext {
}
}
impl AsRef<PolkadotParachainConfiguration> for TestExecutionContext {
fn as_ref(&self) -> &PolkadotParachainConfiguration {
&self.polkadot_parachain_configuration
}
}
impl AsRef<KurtosisConfiguration> for TestExecutionContext {
fn as_ref(&self) -> &KurtosisConfiguration {
&self.lighthouse_configuration
}
}
impl AsRef<KitchensinkConfiguration> for TestExecutionContext {
fn as_ref(&self) -> &KitchensinkConfiguration {
&self.kitchensink_configuration
@@ -301,6 +461,104 @@ impl AsRef<ReportConfiguration> for TestExecutionContext {
}
}
impl Default for BenchmarkingContext {
fn default() -> Self {
Self::parse_from(["execution-context"])
}
}
impl AsRef<WorkingDirectoryConfiguration> for BenchmarkingContext {
fn as_ref(&self) -> &WorkingDirectoryConfiguration {
&self.working_directory
}
}
impl AsRef<CorpusConfiguration> for BenchmarkingContext {
fn as_ref(&self) -> &CorpusConfiguration {
&self.corpus_configuration
}
}
impl AsRef<SolcConfiguration> for BenchmarkingContext {
fn as_ref(&self) -> &SolcConfiguration {
&self.solc_configuration
}
}
impl AsRef<ResolcConfiguration> for BenchmarkingContext {
fn as_ref(&self) -> &ResolcConfiguration {
&self.resolc_configuration
}
}
impl AsRef<GethConfiguration> for BenchmarkingContext {
fn as_ref(&self) -> &GethConfiguration {
&self.geth_configuration
}
}
impl AsRef<KurtosisConfiguration> for BenchmarkingContext {
fn as_ref(&self) -> &KurtosisConfiguration {
&self.lighthouse_configuration
}
}
impl AsRef<PolkadotParachainConfiguration> for BenchmarkingContext {
fn as_ref(&self) -> &PolkadotParachainConfiguration {
&self.polkadot_parachain_configuration
}
}
impl AsRef<KitchensinkConfiguration> for BenchmarkingContext {
fn as_ref(&self) -> &KitchensinkConfiguration {
&self.kitchensink_configuration
}
}
impl AsRef<ReviveDevNodeConfiguration> for BenchmarkingContext {
fn as_ref(&self) -> &ReviveDevNodeConfiguration {
&self.revive_dev_node_configuration
}
}
impl AsRef<EthRpcConfiguration> for BenchmarkingContext {
fn as_ref(&self) -> &EthRpcConfiguration {
&self.eth_rpc_configuration
}
}
impl AsRef<WalletConfiguration> for BenchmarkingContext {
fn as_ref(&self) -> &WalletConfiguration {
&self.wallet_configuration
}
}
impl AsRef<ConcurrencyConfiguration> for BenchmarkingContext {
fn as_ref(&self) -> &ConcurrencyConfiguration {
&self.concurrency_configuration
}
}
impl AsRef<CompilationConfiguration> for BenchmarkingContext {
fn as_ref(&self) -> &CompilationConfiguration {
&self.compilation_configuration
}
}
impl AsRef<ReportConfiguration> for BenchmarkingContext {
fn as_ref(&self) -> &ReportConfiguration {
&self.report_configuration
}
}
/// A set of configuration parameters for the corpus files to use for the execution.
#[derive(Clone, Debug, Parser, Serialize)]
pub struct CorpusConfiguration {
/// A list of test corpus JSON files to be tested.
#[arg(short = 'c', long = "corpus")]
pub paths: Vec<PathBuf>,
}
/// A set of configuration parameters for Solc.
#[derive(Clone, Debug, Parser, Serialize)]
pub struct SolcConfiguration {
@@ -321,6 +579,30 @@ pub struct ResolcConfiguration {
pub path: PathBuf,
}
/// A set of configuration parameters for Polkadot Parachain.
#[derive(Clone, Debug, Parser, Serialize)]
pub struct PolkadotParachainConfiguration {
/// Specifies the path of the polkadot-parachain node to be used by the tool.
///
/// If this is not specified, then the tool assumes that it should use the polkadot-parachain binary
/// that's provided in the user's $PATH.
#[clap(
id = "polkadot-parachain.path",
long = "polkadot-parachain.path",
default_value = "polkadot-parachain"
)]
pub path: PathBuf,
/// The amount of time to wait upon startup before considering that the node timed out.
#[clap(
id = "polkadot-parachain.start-timeout-ms",
long = "polkadot-parachain.start-timeout-ms",
default_value = "5000",
value_parser = parse_duration
)]
pub start_timeout_ms: Duration,
}
/// A set of configuration parameters for Geth.
#[derive(Clone, Debug, Parser, Serialize)]
pub struct GethConfiguration {
@@ -335,12 +617,27 @@ pub struct GethConfiguration {
#[clap(
id = "geth.start-timeout-ms",
long = "geth.start-timeout-ms",
default_value = "5000",
default_value = "30000",
value_parser = parse_duration
)]
pub start_timeout_ms: Duration,
}
/// A set of configuration parameters for kurtosis.
#[derive(Clone, Debug, Parser, Serialize)]
pub struct KurtosisConfiguration {
/// Specifies the path of the kurtosis node to be used by the tool.
///
/// If this is not specified, then the tool assumes that it should use the kurtosis binary that's
/// provided in the user's $PATH.
#[clap(
id = "kurtosis.path",
long = "kurtosis.path",
default_value = "kurtosis"
)]
pub path: PathBuf,
}
/// A set of configuration parameters for Kitchensink.
#[derive(Clone, Debug, Parser, Serialize)]
pub struct KitchensinkConfiguration {
@@ -359,14 +656,10 @@ pub struct KitchensinkConfiguration {
#[clap(
id = "kitchensink.start-timeout-ms",
long = "kitchensink.start-timeout-ms",
default_value = "5000",
default_value = "30000",
value_parser = parse_duration
)]
pub start_timeout_ms: Duration,
/// This configures the tool to use Kitchensink instead of using the revive-dev-node.
#[clap(long = "kitchensink.dont-use-dev-node")]
pub use_kitchensink: bool,
}
/// A set of configuration parameters for the revive dev node.
@@ -387,10 +680,18 @@ pub struct ReviveDevNodeConfiguration {
#[clap(
id = "revive-dev-node.start-timeout-ms",
long = "revive-dev-node.start-timeout-ms",
default_value = "5000",
default_value = "30000",
value_parser = parse_duration
)]
pub start_timeout_ms: Duration,
/// The consensus to use for the spawned revive-dev-node.
#[clap(
id = "revive-dev-node.consensus",
long = "revive-dev-node.consensus",
default_value = "instant-seal"
)]
pub consensus: String,
}
/// A set of configuration parameters for the ETH RPC.
@@ -407,14 +708,14 @@ pub struct EthRpcConfiguration {
#[clap(
id = "eth-rpc.start-timeout-ms",
long = "eth-rpc.start-timeout-ms",
default_value = "5000",
default_value = "30000",
value_parser = parse_duration
)]
pub start_timeout_ms: Duration,
}
/// A set of configuration parameters for the genesis.
#[derive(Clone, Debug, Parser, Serialize)]
#[derive(Clone, Debug, Default, Parser, Serialize)]
pub struct GenesisConfiguration {
/// Specifies the path of the genesis file to use for the nodes that are started.
///
@@ -431,7 +732,7 @@ pub struct GenesisConfiguration {
impl GenesisConfiguration {
pub fn genesis(&self) -> anyhow::Result<&Genesis> {
static DEFAULT_GENESIS: LazyLock<Genesis> = LazyLock::new(|| {
let genesis = include_str!("../../../genesis.json");
let genesis = include_str!("../../../assets/dev-genesis.json");
serde_json::from_str(genesis).unwrap()
});
@@ -466,7 +767,7 @@ pub struct WalletConfiguration {
/// its wallet signers. With a value of N, private keys (0, N] will be added to the signer set
/// of the node.
#[clap(long = "wallet.additional-keys", default_value_t = 100_000)]
additional_keys: usize,
pub additional_keys: usize,
/// The wallet object that will be used.
#[clap(skip)]
@@ -490,6 +791,10 @@ impl WalletConfiguration {
})
.clone()
}
pub fn highest_private_key_exclusive(&self) -> U256 {
U256::try_from(self.additional_keys).unwrap()
}
}
fn serialize_private_key<S>(value: &PrivateKeySigner, serializer: S) -> Result<S::Ok, S::Error>
@@ -654,4 +959,32 @@ pub enum TestingPlatform {
Geth,
/// The kitchensink runtime provides the PolkaVM (PVM) based node implementation.
Kitchensink,
/// A polkadot/Substrate based network
Zombienet,
}
/// The output format to use for the test execution output.
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Serialize,
ValueEnum,
EnumString,
Display,
AsRefStr,
IntoStaticStr,
)]
#[strum(serialize_all = "kebab-case")]
pub enum OutputFormat {
/// The legacy format that was used in the past for the output.
Legacy,
/// An output format that looks heavily resembles the output from `cargo test`.
CargoTestLike,
}
+1
View File
@@ -21,6 +21,7 @@ revive-dt-node = { workspace = true }
revive-dt-node-interaction = { workspace = true }
revive-dt-report = { workspace = true }
ansi_term = { workspace = true }
alloy = { workspace = true }
anyhow = { workspace = true }
bson = { workspace = true }
@@ -0,0 +1,758 @@
use std::{
collections::HashMap,
ops::ControlFlow,
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
},
time::Duration,
};
use alloy::{
hex,
json_abi::JsonAbi,
network::{Ethereum, TransactionBuilder},
primitives::{Address, TxHash, U256},
rpc::types::{
TransactionReceipt, TransactionRequest,
trace::geth::{
CallFrame, GethDebugBuiltInTracerType, GethDebugTracerConfig, GethDebugTracerType,
GethDebugTracingOptions,
},
},
};
use anyhow::{Context as _, Result, bail};
use futures::TryFutureExt;
use indexmap::IndexMap;
use revive_dt_common::{
futures::{PollingWaitBehavior, poll},
types::PrivateKeyAllocator,
};
use revive_dt_format::{
metadata::{ContractInstance, ContractPathAndIdent},
steps::{
AllocateAccountStep, BalanceAssertionStep, Calldata, EtherValue, FunctionCallStep, Method,
RepeatStep, Step, StepAddress, StepIdx, StepPath, StorageEmptyAssertionStep,
},
traits::{ResolutionContext, ResolverApi},
};
use tokio::sync::{Mutex, OnceCell, mpsc::UnboundedSender};
use tracing::{Instrument, Span, debug, error, field::display, info, info_span, instrument};
use crate::{
differential_benchmarks::{ExecutionState, WatcherEvent},
helpers::{CachedCompiler, TestDefinition, TestPlatformInformation},
};
static DRIVER_COUNT: AtomicUsize = AtomicUsize::new(0);
/// The differential tests driver for a single platform.
pub struct Driver<'a, I> {
/// The id of the driver.
driver_id: usize,
/// The information of the platform that this driver is for.
platform_information: &'a TestPlatformInformation<'a>,
/// The resolver of the platform.
resolver: Arc<dyn ResolverApi + 'a>,
/// The definition of the test that the driver is instructed to execute.
test_definition: &'a TestDefinition<'a>,
/// The private key allocator used by this driver and other drivers when account allocations are
/// needed.
private_key_allocator: Arc<Mutex<PrivateKeyAllocator>>,
/// The execution state associated with the platform.
execution_state: ExecutionState,
/// The send side of the watcher's unbounded channel associated with this driver.
watcher_tx: UnboundedSender<WatcherEvent>,
/// The number of steps that were executed on the driver.
steps_executed: usize,
/// This is the queue of steps that are to be executed by the driver for this test case. Each
/// time `execute_step` is called one of the steps is executed.
steps_iterator: I,
}
impl<'a, I> Driver<'a, I>
where
I: Iterator<Item = (StepPath, Step)>,
{
// region:Constructors & Initialization
pub async fn new(
platform_information: &'a TestPlatformInformation<'a>,
test_definition: &'a TestDefinition<'a>,
private_key_allocator: Arc<Mutex<PrivateKeyAllocator>>,
cached_compiler: &CachedCompiler<'a>,
watcher_tx: UnboundedSender<WatcherEvent>,
steps: I,
) -> Result<Self> {
let mut this = Driver {
driver_id: DRIVER_COUNT.fetch_add(1, Ordering::SeqCst),
platform_information,
resolver: platform_information
.node
.resolver()
.await
.context("Failed to create resolver")?,
test_definition,
private_key_allocator,
execution_state: ExecutionState::empty(),
steps_executed: 0,
steps_iterator: steps,
watcher_tx,
};
this.init_execution_state(cached_compiler)
.await
.context("Failed to initialize the execution state of the platform")?;
Ok(this)
}
async fn init_execution_state(&mut self, cached_compiler: &CachedCompiler<'a>) -> Result<()> {
let compiler_output = cached_compiler
.compile_contracts(
self.test_definition.metadata,
self.test_definition.metadata_file_path,
self.test_definition.mode.clone(),
None,
self.platform_information.compiler.as_ref(),
self.platform_information.platform,
&self.platform_information.reporter,
)
.await
.inspect_err(|err| error!(?err, "Pre-linking compilation failed"))
.context("Failed to produce the pre-linking compiled contracts")?;
let mut deployed_libraries = None::<HashMap<_, _>>;
let mut contract_sources = self
.test_definition
.metadata
.contract_sources()
.inspect_err(|err| error!(?err, "Failed to retrieve contract sources from metadata"))
.context("Failed to get the contract instances from the metadata file")?;
for library_instance in self
.test_definition
.metadata
.libraries
.iter()
.flatten()
.flat_map(|(_, map)| map.values())
{
debug!(%library_instance, "Deploying Library Instance");
let ContractPathAndIdent {
contract_source_path: library_source_path,
contract_ident: library_ident,
} = contract_sources
.remove(library_instance)
.context("Failed to get the contract sources of the contract instance")?;
let (code, abi) = compiler_output
.contracts
.get(&library_source_path)
.and_then(|contracts| contracts.get(library_ident.as_str()))
.context("Failed to get the code and abi for the instance")?;
let code = alloy::hex::decode(code)?;
// Getting the deployer address from the cases themselves. This is to ensure
// that we're doing the deployments from different accounts and therefore we're
// not slowed down by the nonce.
let deployer_address = self
.test_definition
.case
.steps
.iter()
.filter_map(|step| match step {
Step::FunctionCall(input) => input.caller.as_address().copied(),
Step::BalanceAssertion(..) => None,
Step::StorageEmptyAssertion(..) => None,
Step::Repeat(..) => None,
Step::AllocateAccount(..) => None,
})
.next()
.unwrap_or(FunctionCallStep::default_caller_address());
let tx = TransactionBuilder::<Ethereum>::with_deploy_code(
TransactionRequest::default().from(deployer_address),
code,
);
let receipt = self
.execute_transaction(tx)
.and_then(|(_, receipt_fut)| receipt_fut)
.await
.inspect_err(|err| {
error!(
?err,
%library_instance,
"Failed to deploy the library"
)
})?;
debug!(?library_instance, "Deployed library");
let library_address = receipt
.contract_address
.expect("Failed to deploy the library");
deployed_libraries.get_or_insert_default().insert(
library_instance.clone(),
(library_ident.clone(), library_address, abi.clone()),
);
}
let compiler_output = cached_compiler
.compile_contracts(
self.test_definition.metadata,
self.test_definition.metadata_file_path,
self.test_definition.mode.clone(),
deployed_libraries.as_ref(),
self.platform_information.compiler.as_ref(),
self.platform_information.platform,
&self.platform_information.reporter,
)
.await
.inspect_err(|err| error!(?err, "Post-linking compilation failed"))
.context("Failed to compile the post-link contracts")?;
self.execution_state = ExecutionState::new(
compiler_output.contracts,
deployed_libraries.unwrap_or_default(),
);
Ok(())
}
// endregion:Constructors & Initialization
// region:Step Handling
pub async fn execute_all(mut self) -> Result<usize> {
while let Some(result) = self.execute_next_step().await {
result?
}
Ok(self.steps_executed)
}
pub async fn execute_next_step(&mut self) -> Option<Result<()>> {
let (step_path, step) = self.steps_iterator.next()?;
info!(%step_path, "Executing Step");
Some(
self.execute_step(&step_path, &step)
.await
.inspect(|_| info!(%step_path, "Step execution succeeded"))
.inspect_err(|err| error!(%step_path, ?err, "Step execution failed")),
)
}
#[instrument(
level = "info",
skip_all,
fields(
driver_id = self.driver_id,
%step_path,
),
err(Debug),
)]
async fn execute_step(&mut self, step_path: &StepPath, step: &Step) -> Result<()> {
let steps_executed = match step {
Step::FunctionCall(step) => self
.execute_function_call(step_path, step.as_ref())
.await
.context("Function call step Failed"),
Step::Repeat(step) => self
.execute_repeat_step(step_path, step.as_ref())
.await
.context("Repetition Step Failed"),
Step::AllocateAccount(step) => self
.execute_account_allocation(step_path, step.as_ref())
.await
.context("Account Allocation Step Failed"),
// The following steps are disabled in the benchmarking driver.
Step::BalanceAssertion(..) | Step::StorageEmptyAssertion(..) => Ok(0),
}?;
self.steps_executed += steps_executed;
Ok(())
}
#[instrument(level = "info", skip_all, fields(driver_id = self.driver_id))]
pub async fn execute_function_call(
&mut self,
_: &StepPath,
step: &FunctionCallStep,
) -> Result<usize> {
let deployment_receipts = self
.handle_function_call_contract_deployment(step)
.await
.context("Failed to deploy contracts for the function call step")?;
let transaction_hash = self
.handle_function_call_execution(step, deployment_receipts)
.await
.context("Failed to handle the function call execution")?;
self.handle_function_call_variable_assignment(step, transaction_hash)
.await
.context("Failed to handle function call variable assignment")?;
Ok(1)
}
async fn handle_function_call_contract_deployment(
&mut self,
step: &FunctionCallStep,
) -> Result<HashMap<ContractInstance, TransactionReceipt>> {
let mut instances_we_must_deploy = IndexMap::<ContractInstance, bool>::new();
for instance in step.find_all_contract_instances().into_iter() {
if !self
.execution_state
.deployed_contracts
.contains_key(&instance)
{
instances_we_must_deploy.entry(instance).or_insert(false);
}
}
if let Method::Deployer = step.method {
instances_we_must_deploy.swap_remove(&step.instance);
instances_we_must_deploy.insert(step.instance.clone(), true);
}
let mut receipts = HashMap::new();
for (instance, deploy_with_constructor_arguments) in instances_we_must_deploy.into_iter() {
let calldata = deploy_with_constructor_arguments.then_some(&step.calldata);
let value = deploy_with_constructor_arguments
.then_some(step.value)
.flatten();
let caller = {
let context = self.default_resolution_context();
step.caller
.resolve_address(self.resolver.as_ref(), context)
.await?
};
if let (_, _, Some(receipt)) = self
.get_or_deploy_contract_instance(&instance, caller, calldata, value)
.await
.context("Failed to get or deploy contract instance during input execution")?
{
receipts.insert(instance.clone(), receipt);
}
}
Ok(receipts)
}
async fn handle_function_call_execution(
&mut self,
step: &FunctionCallStep,
mut deployment_receipts: HashMap<ContractInstance, TransactionReceipt>,
) -> Result<TxHash> {
match step.method {
// This step was already executed when `handle_step` was called. We just need to
// lookup the transaction receipt in this case and continue on.
Method::Deployer => deployment_receipts
.remove(&step.instance)
.context("Failed to find deployment receipt for constructor call")
.map(|receipt| receipt.transaction_hash),
Method::Fallback | Method::FunctionName(_) => {
let tx = step
.as_transaction(self.resolver.as_ref(), self.default_resolution_context())
.await?;
Ok(self.execute_transaction(tx).await?.0)
}
}
}
async fn handle_function_call_call_frame_tracing(
&mut self,
tx_hash: TxHash,
) -> Result<CallFrame> {
self.platform_information
.node
.trace_transaction(
tx_hash,
GethDebugTracingOptions {
tracer: Some(GethDebugTracerType::BuiltInTracer(
GethDebugBuiltInTracerType::CallTracer,
)),
tracer_config: GethDebugTracerConfig(serde_json::json! {{
"onlyTopCall": true,
"withLog": false,
"withStorage": false,
"withMemory": false,
"withStack": false,
"withReturnData": true
}}),
..Default::default()
},
)
.await
.map(|trace| {
trace
.try_into_call_frame()
.expect("Impossible - we requested a callframe trace so we must get it back")
})
}
async fn handle_function_call_variable_assignment(
&mut self,
step: &FunctionCallStep,
tx_hash: TxHash,
) -> Result<()> {
let Some(ref assignments) = step.variable_assignments else {
return Ok(());
};
// Handling the return data variable assignments.
let callframe = OnceCell::new();
for (variable_name, output_word) in assignments.return_data.iter().zip(
callframe
.get_or_try_init(|| self.handle_function_call_call_frame_tracing(tx_hash))
.await
.context("Failed to get the callframe trace for transaction")?
.output
.as_ref()
.unwrap_or_default()
.to_vec()
.chunks(32),
) {
let value = U256::from_be_slice(output_word);
self.execution_state
.variables
.insert(variable_name.clone(), value);
tracing::info!(
variable_name,
variable_value = hex::encode(value.to_be_bytes::<32>()),
"Assigned variable"
);
}
Ok(())
}
#[instrument(level = "info", skip_all, fields(driver_id = self.driver_id))]
pub async fn execute_balance_assertion(
&mut self,
_: &StepPath,
_: &BalanceAssertionStep,
) -> anyhow::Result<usize> {
// Kept empty intentionally for the benchmark driver.
Ok(1)
}
#[instrument(level = "info", skip_all, fields(driver_id = self.driver_id), err(Debug))]
async fn execute_storage_empty_assertion_step(
&mut self,
_: &StepPath,
_: &StorageEmptyAssertionStep,
) -> Result<usize> {
// Kept empty intentionally for the benchmark driver.
Ok(1)
}
#[instrument(level = "info", skip_all, fields(driver_id = self.driver_id), err(Debug))]
async fn execute_repeat_step(
&mut self,
step_path: &StepPath,
step: &RepeatStep,
) -> Result<usize> {
let tasks = (0..step.repeat)
.map(|_| Driver {
driver_id: DRIVER_COUNT.fetch_add(1, Ordering::SeqCst),
platform_information: self.platform_information,
resolver: self.resolver.clone(),
test_definition: self.test_definition,
private_key_allocator: self.private_key_allocator.clone(),
execution_state: self.execution_state.clone(),
steps_executed: 0,
steps_iterator: {
let steps = step
.steps
.iter()
.cloned()
.enumerate()
.map(|(step_idx, step)| {
let step_idx = StepIdx::new(step_idx);
let step_path = step_path.append(step_idx);
(step_path, step)
})
.collect::<Vec<_>>();
steps.into_iter()
},
watcher_tx: self.watcher_tx.clone(),
})
.map(|driver| driver.execute_all());
// TODO: Determine how we want to know the `ignore_block_before` and if it's through the
// receipt and how this would impact the architecture and the possibility of us not waiting
// for receipts in the future.
self.watcher_tx
.send(WatcherEvent::RepetitionStartEvent {
ignore_block_before: 0,
})
.context("Failed to send message on the watcher's tx")?;
let res = futures::future::try_join_all(tasks)
.await
.context("Repetition execution failed")?;
Ok(res.into_iter().sum())
}
#[instrument(level = "info", fields(driver_id = self.driver_id), skip_all, err(Debug))]
pub async fn execute_account_allocation(
&mut self,
_: &StepPath,
step: &AllocateAccountStep,
) -> Result<usize> {
let Some(variable_name) = step.variable_name.strip_prefix("$VARIABLE:") else {
bail!("Account allocation must start with $VARIABLE:");
};
let private_key = self
.private_key_allocator
.lock()
.await
.allocate()
.context("Account allocation through the private key allocator failed")?;
let account = private_key.address();
let variable = U256::from_be_slice(account.0.as_slice());
self.execution_state
.variables
.insert(variable_name.to_string(), variable);
Ok(1)
}
// endregion:Step Handling
// region:Contract Deployment
#[instrument(
level = "info",
skip_all,
fields(
driver_id = self.driver_id,
%contract_instance,
%deployer
),
err(Debug),
)]
async fn get_or_deploy_contract_instance(
&mut self,
contract_instance: &ContractInstance,
deployer: Address,
calldata: Option<&Calldata>,
value: Option<EtherValue>,
) -> Result<(Address, JsonAbi, Option<TransactionReceipt>)> {
if let Some((_, address, abi)) = self
.execution_state
.deployed_contracts
.get(contract_instance)
{
info!(
%address,
"Contract instance already deployed."
);
Ok((*address, abi.clone(), None))
} else {
info!("Contract instance requires deployment.");
let (address, abi, receipt) = self
.deploy_contract(contract_instance, deployer, calldata, value)
.await
.context("Failed to deploy contract")?;
info!(
%address,
"Contract instance has been deployed."
);
Ok((address, abi, Some(receipt)))
}
}
#[instrument(
level = "info",
skip_all,
fields(
driver_id = self.driver_id,
%contract_instance,
%deployer
),
err(Debug),
)]
async fn deploy_contract(
&mut self,
contract_instance: &ContractInstance,
deployer: Address,
calldata: Option<&Calldata>,
value: Option<EtherValue>,
) -> Result<(Address, JsonAbi, TransactionReceipt)> {
let Some(ContractPathAndIdent {
contract_source_path,
contract_ident,
}) = self
.test_definition
.metadata
.contract_sources()?
.remove(contract_instance)
else {
anyhow::bail!(
"Contract source not found for instance {:?}",
contract_instance
)
};
let Some((code, abi)) = self
.execution_state
.compiled_contracts
.get(&contract_source_path)
.and_then(|source_file_contracts| source_file_contracts.get(contract_ident.as_ref()))
.cloned()
else {
anyhow::bail!(
"Failed to find information for contract {:?}",
contract_instance
)
};
let mut code = match alloy::hex::decode(&code) {
Ok(code) => code,
Err(error) => {
tracing::error!(
?error,
contract_source_path = contract_source_path.display().to_string(),
contract_ident = contract_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)
}
};
if let Some(calldata) = calldata {
let calldata = calldata
.calldata(self.resolver.as_ref(), self.default_resolution_context())
.await?;
code.extend(calldata);
}
let tx = {
let tx = TransactionRequest::default().from(deployer);
let tx = match value {
Some(ref value) => tx.value(value.into_inner()),
_ => tx,
};
TransactionBuilder::<Ethereum>::with_deploy_code(tx, code)
};
let receipt = match self
.execute_transaction(tx)
.and_then(|(_, receipt_fut)| receipt_fut)
.await
{
Ok(receipt) => receipt,
Err(error) => {
tracing::error!(?error, "Contract deployment transaction failed.");
return Err(error);
}
};
let Some(address) = receipt.contract_address else {
anyhow::bail!("Contract deployment didn't return an address");
};
tracing::info!(
instance_name = ?contract_instance,
instance_address = ?address,
"Deployed contract"
);
self.platform_information
.reporter
.report_contract_deployed_event(contract_instance.clone(), address)?;
self.execution_state.deployed_contracts.insert(
contract_instance.clone(),
(contract_ident, address, abi.clone()),
);
Ok((address, abi, receipt))
}
#[instrument(level = "info", fields(driver_id = self.driver_id), skip_all)]
async fn step_address_auto_deployment(
&mut self,
step_address: &StepAddress,
) -> Result<Address> {
match step_address {
StepAddress::Address(address) => Ok(*address),
StepAddress::ResolvableAddress(resolvable) => {
let Some(instance) = resolvable
.strip_suffix(".address")
.map(ContractInstance::new)
else {
bail!("Not an address variable");
};
self.get_or_deploy_contract_instance(
&instance,
FunctionCallStep::default_caller_address(),
None,
None,
)
.await
.map(|v| v.0)
}
}
}
// endregion:Contract Deployment
// region:Resolution & Resolver
fn default_resolution_context(&self) -> ResolutionContext<'_> {
ResolutionContext::default()
.with_deployed_contracts(&self.execution_state.deployed_contracts)
.with_variables(&self.execution_state.variables)
}
// endregion:Resolution & Resolver
// region:Transaction Execution
/// Executes the transaction on the driver's node with some custom waiting logic for the receipt
#[instrument(
level = "info",
skip_all,
fields(driver_id = self.driver_id, transaction_hash = tracing::field::Empty)
)]
async fn execute_transaction(
&self,
transaction: TransactionRequest,
) -> anyhow::Result<(TxHash, impl Future<Output = Result<TransactionReceipt>>)> {
let node = self.platform_information.node;
let transaction_hash = node
.submit_transaction(transaction)
.await
.context("Failed to submit transaction")?;
Span::current().record("transaction_hash", display(transaction_hash));
info!("Submitted transaction");
self.watcher_tx
.send(WatcherEvent::SubmittedTransaction { transaction_hash })
.context("Failed to send the transaction hash to the watcher")?;
Ok((transaction_hash, async move {
info!("Starting to poll for transaction receipt");
poll(
Duration::from_secs(30 * 60),
PollingWaitBehavior::Constant(Duration::from_secs(1)),
|| {
async move {
match node.get_receipt(transaction_hash).await {
Ok(receipt) => {
info!("Polling succeeded, receipt found");
Ok(ControlFlow::Break(receipt))
}
Err(_) => Ok(ControlFlow::Continue(())),
}
}
.instrument(info_span!("Polling for receipt"))
},
)
.instrument(info_span!("Polling for receipt", %transaction_hash))
.await
.inspect(|_| info!("Found the transaction receipt"))
}))
}
// endregion:Transaction Execution
}
@@ -0,0 +1,180 @@
//! The main entry point for differential benchmarking.
use std::{collections::BTreeMap, sync::Arc};
use anyhow::Context as _;
use futures::{FutureExt, StreamExt};
use revive_dt_common::types::PrivateKeyAllocator;
use revive_dt_core::Platform;
use revive_dt_format::steps::{Step, StepIdx, StepPath};
use tokio::sync::Mutex;
use tracing::{Instrument, error, info, info_span, instrument, warn};
use revive_dt_config::{BenchmarkingContext, Context};
use revive_dt_report::Reporter;
use crate::{
differential_benchmarks::{Driver, Watcher, WatcherEvent},
helpers::{CachedCompiler, NodePool, collect_metadata_files, create_test_definitions_stream},
};
/// Handles the differential testing executing it according to the information defined in the
/// context
#[instrument(level = "info", err(Debug), skip_all)]
pub async fn handle_differential_benchmarks(
mut context: BenchmarkingContext,
reporter: Reporter,
) -> anyhow::Result<()> {
// A bit of a hack but we need to override the number of nodes specified through the CLI since
// benchmarks can only be run on a single node. Perhaps in the future we'd have a cleaner way to
// do this. But, for the time being, we need to override the cli arguments.
if context.concurrency_configuration.number_of_nodes != 1 {
warn!(
specified_number_of_nodes = context.concurrency_configuration.number_of_nodes,
updated_number_of_nodes = 1,
"Invalid number of nodes specified through the CLI. Benchmarks can only be run on a single node. Updated the arguments."
);
context.concurrency_configuration.number_of_nodes = 1;
};
let full_context = Context::Benchmark(Box::new(context.clone()));
// Discover all of the metadata files that are defined in the context.
let metadata_files = collect_metadata_files(&context)
.context("Failed to collect metadata files for differential testing")?;
info!(len = metadata_files.len(), "Discovered metadata files");
// Discover the list of platforms that the tests should run on based on the context.
let platforms = context
.platforms
.iter()
.copied()
.map(Into::<&dyn Platform>::into)
.collect::<Vec<_>>();
// Starting the nodes of the various platforms specified in the context. Note that we use the
// node pool since it contains all of the code needed to spawn nodes from A to Z and therefore
// it's the preferred way for us to start nodes even when we're starting just a single node. The
// added overhead from it is quite small (performance wise) since it's involved only when we're
// creating the test definitions, but it might have other maintenance overhead as it obscures
// the fact that only a single node is spawned.
let platforms_and_nodes = {
let mut map = BTreeMap::new();
for platform in platforms.iter() {
let platform_identifier = platform.platform_identifier();
let node_pool = NodePool::new(full_context.clone(), *platform)
.await
.inspect_err(|err| {
error!(
?err,
%platform_identifier,
"Failed to initialize the node pool for the platform."
)
})
.context("Failed to initialize the node pool")?;
map.insert(platform_identifier, (*platform, node_pool));
}
map
};
info!("Spawned the platform nodes");
// Preparing test definitions for the execution.
let test_definitions = create_test_definitions_stream(
&full_context,
metadata_files.iter(),
&platforms_and_nodes,
reporter.clone(),
)
.await
.collect::<Vec<_>>()
.await;
info!(len = test_definitions.len(), "Created test definitions");
// Creating the objects that will be shared between the various runs. The cached compiler is the
// only one at the current moment of time that's safe to share between runs.
let cached_compiler = CachedCompiler::new(
context
.working_directory
.as_path()
.join("compilation_cache"),
context
.compilation_configuration
.invalidate_compilation_cache,
)
.await
.map(Arc::new)
.context("Failed to initialize cached compiler")?;
// Note: we do not want to run all of the workloads concurrently on all platforms. Rather, we'd
// like to run all of the workloads for one platform, and then the next sequentially as we'd
// like for the effect of concurrency to be minimized when we're doing the benchmarking.
for platform in platforms.iter() {
let platform_identifier = platform.platform_identifier();
let span = info_span!("Benchmarking for the platform", %platform_identifier);
let _guard = span.enter();
for test_definition in test_definitions.iter() {
let platform_information = &test_definition.platforms[&platform_identifier];
let span = info_span!(
"Executing workload",
metadata_file_path = %test_definition.metadata_file_path.display(),
case_idx = %test_definition.case_idx,
mode = %test_definition.mode,
);
let _guard = span.enter();
// Initializing all of the components requires to execute this particular workload.
let private_key_allocator = Arc::new(Mutex::new(PrivateKeyAllocator::new(
context.wallet_configuration.highest_private_key_exclusive(),
)));
let (watcher, watcher_tx) = Watcher::new(
platform_identifier,
platform_information
.node
.subscribe_to_full_blocks_information()
.await
.context("Failed to subscribe to full blocks information from the node")?,
);
let driver = Driver::new(
platform_information,
test_definition,
private_key_allocator,
cached_compiler.as_ref(),
watcher_tx.clone(),
test_definition
.case
.steps_iterator_for_benchmarks(context.default_repetition_count)
.enumerate()
.map(|(step_idx, step)| -> (StepPath, Step) {
(StepPath::new(vec![StepIdx::new(step_idx)]), step)
}),
)
.await
.context("Failed to create the benchmarks driver")?;
futures::future::try_join(
watcher.run(),
driver
.execute_all()
.instrument(info_span!("Executing Benchmarks", %platform_identifier))
.inspect(|_| {
info!("All transactions submitted - driver completed execution");
watcher_tx
.send(WatcherEvent::AllTransactionsSubmitted)
.unwrap()
}),
)
.await
.context("Failed to run the driver and executor")
.inspect(|(_, steps_executed)| info!(steps_executed, "Workload Execution Succeeded"))
.inspect_err(|err| error!(?err, "Workload Execution Failed"))?;
}
}
Ok(())
}
@@ -0,0 +1,43 @@
use std::{collections::HashMap, path::PathBuf};
use alloy::{
json_abi::JsonAbi,
primitives::{Address, U256},
};
use revive_dt_format::metadata::{ContractIdent, ContractInstance};
#[derive(Clone)]
/// The state associated with the test execution of one of the workloads.
pub struct ExecutionState {
/// The compiled contracts, these contracts have been compiled and have had the libraries linked
/// against them and therefore they're ready to be deployed on-demand.
pub compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
/// A map of all of the deployed contracts and information about them.
pub deployed_contracts: HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>,
/// This map stores the variables used for each one of the cases contained in the metadata file.
pub variables: HashMap<String, U256>,
}
impl ExecutionState {
pub fn new(
compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
deployed_contracts: HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>,
) -> Self {
Self {
compiled_contracts,
deployed_contracts,
variables: Default::default(),
}
}
pub fn empty() -> Self {
Self {
compiled_contracts: Default::default(),
deployed_contracts: Default::default(),
variables: Default::default(),
}
}
}
@@ -0,0 +1,9 @@
mod driver;
mod entry_point;
mod execution_state;
mod watcher;
pub use driver::*;
pub use entry_point::*;
pub use execution_state::*;
pub use watcher::*;
@@ -0,0 +1,195 @@
use std::{collections::HashSet, pin::Pin, sync::Arc};
use alloy::primitives::{BlockNumber, TxHash};
use anyhow::Result;
use futures::{Stream, StreamExt};
use revive_dt_common::types::PlatformIdentifier;
use revive_dt_node_interaction::MinedBlockInformation;
use tokio::sync::{
RwLock,
mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel},
};
use tracing::{info, instrument};
/// This struct defines the watcher used in the benchmarks. A watcher is only valid for 1 workload
/// and MUST NOT be re-used between workloads since it holds important internal state for a given
/// workload and is not designed for reuse.
pub struct Watcher {
/// The identifier of the platform that this watcher is for.
platform_identifier: PlatformIdentifier,
/// The receive side of the channel that all of the drivers and various other parts of the code
/// send events to the watcher on.
rx: UnboundedReceiver<WatcherEvent>,
/// This is a stream of the blocks that were mined by the node. This is for a single platform
/// and a single node from that platform.
blocks_stream: Pin<Box<dyn Stream<Item = MinedBlockInformation>>>,
}
impl Watcher {
pub fn new(
platform_identifier: PlatformIdentifier,
blocks_stream: Pin<Box<dyn Stream<Item = MinedBlockInformation>>>,
) -> (Self, UnboundedSender<WatcherEvent>) {
let (tx, rx) = unbounded_channel::<WatcherEvent>();
(
Self {
platform_identifier,
rx,
blocks_stream,
},
tx,
)
}
#[instrument(level = "info", skip_all)]
pub async fn run(mut self) -> Result<()> {
// The first event that the watcher receives must be a `RepetitionStartEvent` that informs
// the watcher of the last block number that it should ignore and what the block number is
// for the first important block that it should look for.
let ignore_block_before = loop {
let Some(WatcherEvent::RepetitionStartEvent {
ignore_block_before,
}) = self.rx.recv().await
else {
continue;
};
break ignore_block_before;
};
// This is the set of the transaction hashes that the watcher should be looking for and
// watch for them in the blocks. The watcher will keep watching for blocks until it sees
// that all of the transactions that it was watching for has been seen in the mined blocks.
let watch_for_transaction_hashes = Arc::new(RwLock::new(HashSet::<TxHash>::new()));
// A boolean that keeps track of whether all of the transactions were submitted or if more
// txs are expected to come through the receive side of the channel. We do not want to rely
// on the channel closing alone for the watcher to know that all of the transactions were
// submitted and for there to be an explicit event sent by the core orchestrator that
// informs the watcher that no further transactions are to be expected and that it can
// safely ignore the channel.
let all_transactions_submitted = Arc::new(RwLock::new(false));
let watcher_event_watching_task = {
let watch_for_transaction_hashes = watch_for_transaction_hashes.clone();
let all_transactions_submitted = all_transactions_submitted.clone();
async move {
while let Some(watcher_event) = self.rx.recv().await {
match watcher_event {
// Subsequent repetition starts are ignored since certain workloads can
// contain nested repetitions and therefore there's no use in doing any
// action if the repetitions are nested.
WatcherEvent::RepetitionStartEvent { .. } => {}
WatcherEvent::SubmittedTransaction { transaction_hash } => {
watch_for_transaction_hashes
.write()
.await
.insert(transaction_hash);
}
WatcherEvent::AllTransactionsSubmitted => {
*all_transactions_submitted.write().await = true;
self.rx.close();
info!("Watcher's Events Watching Task Finished");
break;
}
}
}
}
};
let block_information_watching_task = {
let watch_for_transaction_hashes = watch_for_transaction_hashes.clone();
let all_transactions_submitted = all_transactions_submitted.clone();
let mut blocks_information_stream = self.blocks_stream;
async move {
let mut mined_blocks_information = Vec::new();
// region:TEMPORARY
eprintln!("Watcher information for {}", self.platform_identifier);
eprintln!("block_number,block_timestamp,mined_gas,block_gas_limit,tx_count");
// endregion:TEMPORARY
while let Some(block) = blocks_information_stream.next().await {
// If the block number is equal to or less than the last block before the
// repetition then we ignore it and continue on to the next block.
if block.block_number <= ignore_block_before {
continue;
}
if *all_transactions_submitted.read().await
&& watch_for_transaction_hashes.read().await.is_empty()
{
break;
}
info!(
block_number = block.block_number,
block_tx_count = block.transaction_hashes.len(),
remaining_transactions = watch_for_transaction_hashes.read().await.len(),
"Observed a block"
);
// Remove all of the transaction hashes observed in this block from the txs we
// are currently watching for.
let mut watch_for_transaction_hashes =
watch_for_transaction_hashes.write().await;
for tx_hash in block.transaction_hashes.iter() {
watch_for_transaction_hashes.remove(tx_hash);
}
// region:TEMPORARY
// TODO: The following core is TEMPORARY and will be removed once we have proper
// reporting in place and then it can be removed. This serves as as way of doing
// some very simple reporting for the time being.
eprintln!(
"\"{}\",\"{}\",\"{}\",\"{}\",\"{}\"",
block.block_number,
block.block_timestamp,
block.mined_gas,
block.block_gas_limit,
block.transaction_hashes.len()
);
// endregion:TEMPORARY
mined_blocks_information.push(block);
}
info!("Watcher's Block Watching Task Finished");
mined_blocks_information
}
};
let (_, _) =
futures::future::join(watcher_event_watching_task, block_information_watching_task)
.await;
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum WatcherEvent {
/// Informs the watcher that it should begin watching for the blocks mined by the platforms.
/// Before the watcher receives this event it will not be watching for the mined blocks. The
/// reason behind this is that we do not want the initialization transactions (e.g., contract
/// deployments) to be included in the overall TPS and GPS measurements since these blocks will
/// most likely only contain a single transaction since they're just being used for
/// initialization.
RepetitionStartEvent {
/// This is the block number of the last block seen before the repetition started. This is
/// used to instruct the watcher to ignore all block prior to this block when it starts
/// streaming the blocks.
ignore_block_before: BlockNumber,
},
/// Informs the watcher that a transaction was submitted and that the watcher should watch for a
/// transaction with this hash in the blocks that it watches.
SubmittedTransaction {
/// The hash of the submitted transaction.
transaction_hash: TxHash,
},
/// Informs the watcher that all of the transactions of this benchmark have been submitted and
/// that it can expect to receive no further transaction hashes and not even watch the channel
/// any longer.
AllTransactionsSubmitted,
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,342 @@
//! The main entry point into differential testing.
use std::{
collections::{BTreeMap, BTreeSet},
io::{BufWriter, Write, stderr},
sync::Arc,
time::{Duration, Instant},
};
use ansi_term::{ANSIStrings, Color};
use anyhow::Context as _;
use futures::{FutureExt, StreamExt};
use revive_dt_common::types::PrivateKeyAllocator;
use revive_dt_core::Platform;
use tokio::sync::{Mutex, RwLock, Semaphore};
use tracing::{Instrument, error, info, info_span, instrument};
use revive_dt_config::{Context, OutputFormat, TestExecutionContext};
use revive_dt_report::{Reporter, ReporterEvent, TestCaseStatus};
use crate::{
differential_tests::Driver,
helpers::{CachedCompiler, NodePool, collect_metadata_files, create_test_definitions_stream},
};
/// Handles the differential testing executing it according to the information defined in the
/// context
#[instrument(level = "info", err(Debug), skip_all)]
pub async fn handle_differential_tests(
context: TestExecutionContext,
reporter: Reporter,
) -> anyhow::Result<()> {
let reporter_clone = reporter.clone();
// Discover all of the metadata files that are defined in the context.
let metadata_files = collect_metadata_files(&context)
.context("Failed to collect metadata files for differential testing")?;
info!(len = metadata_files.len(), "Discovered metadata files");
// Discover the list of platforms that the tests should run on based on the context.
let platforms = context
.platforms
.iter()
.copied()
.map(Into::<&dyn Platform>::into)
.collect::<Vec<_>>();
// Starting the nodes of the various platforms specified in the context.
let platforms_and_nodes = {
let mut map = BTreeMap::new();
for platform in platforms.iter() {
let platform_identifier = platform.platform_identifier();
let context = Context::Test(Box::new(context.clone()));
let node_pool = NodePool::new(context, *platform)
.await
.inspect_err(|err| {
error!(
?err,
%platform_identifier,
"Failed to initialize the node pool for the platform."
)
})
.context("Failed to initialize the node pool")?;
map.insert(platform_identifier, (*platform, node_pool));
}
map
};
info!("Spawned the platform nodes");
// Preparing test definitions.
let full_context = Context::Test(Box::new(context.clone()));
let test_definitions = create_test_definitions_stream(
&full_context,
metadata_files.iter(),
&platforms_and_nodes,
reporter.clone(),
)
.await
.collect::<Vec<_>>()
.await;
info!(len = test_definitions.len(), "Created test definitions");
// Creating everything else required for the driver to run.
let cached_compiler = CachedCompiler::new(
context
.working_directory
.as_path()
.join("compilation_cache"),
context
.compilation_configuration
.invalidate_compilation_cache,
)
.await
.map(Arc::new)
.context("Failed to initialize cached compiler")?;
let private_key_allocator = Arc::new(Mutex::new(PrivateKeyAllocator::new(
context.wallet_configuration.highest_private_key_exclusive(),
)));
// Creating the driver and executing all of the steps.
let semaphore = context
.concurrency_configuration
.concurrency_limit()
.map(Semaphore::new)
.map(Arc::new);
let running_task_list = Arc::new(RwLock::new(BTreeSet::<usize>::new()));
let driver_task = futures::future::join_all(test_definitions.iter().enumerate().map(
|(test_id, test_definition)| {
let running_task_list = running_task_list.clone();
let semaphore = semaphore.clone();
let private_key_allocator = private_key_allocator.clone();
let cached_compiler = cached_compiler.clone();
let mode = test_definition.mode.clone();
let span = info_span!(
"Executing Test Case",
test_id,
metadata_file_path = %test_definition.metadata_file_path.display(),
case_idx = %test_definition.case_idx,
mode = %mode,
);
async move {
let permit = match semaphore.as_ref() {
Some(semaphore) => Some(semaphore.acquire().await.expect("Can't fail")),
None => None,
};
running_task_list.write().await.insert(test_id);
let driver = match Driver::new_root(
test_definition,
private_key_allocator,
&cached_compiler,
)
.await
{
Ok(driver) => driver,
Err(error) => {
test_definition
.reporter
.report_test_failed_event(format!("{error:#}"))
.expect("Can't fail");
error!("Test Case Failed");
drop(permit);
running_task_list.write().await.remove(&test_id);
return;
}
};
info!("Created the driver for the test case");
match driver.execute_all().await {
Ok(steps_executed) => test_definition
.reporter
.report_test_succeeded_event(steps_executed)
.expect("Can't fail"),
Err(error) => {
test_definition
.reporter
.report_test_failed_event(format!("{error:#}"))
.expect("Can't fail");
error!("Test Case Failed");
}
};
info!("Finished the execution of the test case");
drop(permit);
running_task_list.write().await.remove(&test_id);
}
.instrument(span)
},
))
.inspect(|_| {
info!("Finished executing all test cases");
reporter_clone
.report_completion_event()
.expect("Can't fail")
});
let cli_reporting_task = start_cli_reporting_task(context.output_format, reporter);
tokio::task::spawn(async move {
loop {
let remaining_tasks = running_task_list.read().await;
info!(
count = remaining_tasks.len(),
?remaining_tasks,
"Remaining Tests"
);
tokio::time::sleep(Duration::from_secs(10)).await
}
});
futures::future::join(driver_task, cli_reporting_task).await;
Ok(())
}
#[allow(irrefutable_let_patterns, clippy::uninlined_format_args)]
async fn start_cli_reporting_task(output_format: OutputFormat, reporter: Reporter) {
let mut aggregator_events_rx = reporter.subscribe().await.expect("Can't fail");
drop(reporter);
let start = Instant::now();
let mut global_success_count = 0;
let mut global_failure_count = 0;
let mut global_ignore_count = 0;
let mut buf = BufWriter::new(stderr());
while let Ok(event) = aggregator_events_rx.recv().await {
let ReporterEvent::MetadataFileSolcModeCombinationExecutionCompleted {
metadata_file_path,
mode,
case_status,
} = event
else {
continue;
};
match output_format {
OutputFormat::Legacy => {
let _ = writeln!(buf, "{} - {}", mode, metadata_file_path.display());
for (case_idx, case_status) in case_status.into_iter() {
let _ = write!(buf, "\tCase Index {case_idx:>3}: ");
let _ = match case_status {
TestCaseStatus::Succeeded { steps_executed } => {
global_success_count += 1;
writeln!(
buf,
"{}",
ANSIStrings(&[
Color::Green.bold().paint("Case Succeeded"),
Color::Green
.paint(format!(" - Steps Executed: {steps_executed}")),
])
)
}
TestCaseStatus::Failed { reason } => {
global_failure_count += 1;
writeln!(
buf,
"{}",
ANSIStrings(&[
Color::Red.bold().paint("Case Failed"),
Color::Red.paint(format!(" - Reason: {}", reason.trim())),
])
)
}
TestCaseStatus::Ignored { reason, .. } => {
global_ignore_count += 1;
writeln!(
buf,
"{}",
ANSIStrings(&[
Color::Yellow.bold().paint("Case Ignored"),
Color::Yellow.paint(format!(" - Reason: {}", reason.trim())),
])
)
}
};
}
let _ = writeln!(buf);
}
OutputFormat::CargoTestLike => {
writeln!(
buf,
"\t{} {} - {}\n",
Color::Green.paint("Running"),
metadata_file_path.display(),
mode
)
.unwrap();
let mut success_count = 0;
let mut failure_count = 0;
let mut ignored_count = 0;
writeln!(buf, "running {} tests", case_status.len()).unwrap();
for (case_idx, case_result) in case_status.iter() {
let status = match case_result {
TestCaseStatus::Succeeded { .. } => {
success_count += 1;
global_success_count += 1;
Color::Green.paint("ok")
}
TestCaseStatus::Failed { reason } => {
failure_count += 1;
global_failure_count += 1;
Color::Red.paint(format!("FAILED, {reason}"))
}
TestCaseStatus::Ignored { reason, .. } => {
ignored_count += 1;
global_ignore_count += 1;
Color::Yellow.paint(format!("ignored, {reason:?}"))
}
};
writeln!(buf, "test case_idx_{} ... {}", case_idx, status).unwrap();
}
writeln!(buf).unwrap();
let status = if failure_count > 0 {
Color::Red.paint("FAILED")
} else {
Color::Green.paint("ok")
};
writeln!(
buf,
"test result: {}. {} passed; {} failed; {} ignored",
status, success_count, failure_count, ignored_count,
)
.unwrap();
writeln!(buf).unwrap()
}
}
}
// Summary at the end.
match output_format {
OutputFormat::Legacy => {
writeln!(
buf,
"{} cases: {} cases succeeded, {} cases failed in {} seconds",
global_success_count + global_failure_count + global_ignore_count,
Color::Green.paint(global_success_count.to_string()),
Color::Red.paint(global_failure_count.to_string()),
start.elapsed().as_secs()
)
.unwrap();
}
OutputFormat::CargoTestLike => {
writeln!(
buf,
"run finished. {} passed; {} failed; {} ignored; finished in {}s",
global_success_count,
global_failure_count,
global_ignore_count,
start.elapsed().as_secs()
)
.unwrap();
}
}
}
@@ -0,0 +1,35 @@
use std::{collections::HashMap, path::PathBuf};
use alloy::{
json_abi::JsonAbi,
primitives::{Address, U256},
};
use revive_dt_format::metadata::{ContractIdent, ContractInstance};
#[derive(Clone)]
/// The state associated with the test execution of one of the tests.
pub struct ExecutionState {
/// The compiled contracts, these contracts have been compiled and have had the libraries linked
/// against them and therefore they're ready to be deployed on-demand.
pub compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
/// A map of all of the deployed contracts and information about them.
pub deployed_contracts: HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>,
/// This map stores the variables used for each one of the cases contained in the metadata file.
pub variables: HashMap<String, U256>,
}
impl ExecutionState {
pub fn new(
compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
deployed_contracts: HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>,
) -> Self {
Self {
compiled_contracts,
deployed_contracts,
variables: Default::default(),
}
}
}
+11
View File
@@ -0,0 +1,11 @@
//! This module contains all of the code responsible for performing differential tests including the
//! driver implementation, state implementation, and the core logic that allows for tests to be
//! executed.
mod driver;
mod entry_point;
mod execution_state;
pub use driver::*;
pub use entry_point::*;
pub use execution_state::*;
-878
View File
@@ -1,878 +0,0 @@
//! The test driver handles the compilation and execution of the test cases.
use std::collections::HashMap;
use std::path::PathBuf;
use alloy::consensus::EMPTY_ROOT_HASH;
use alloy::hex;
use alloy::json_abi::JsonAbi;
use alloy::network::{Ethereum, TransactionBuilder};
use alloy::primitives::{TxHash, U256};
use alloy::rpc::types::TransactionReceipt;
use alloy::rpc::types::trace::geth::{
CallFrame, GethDebugBuiltInTracerType, GethDebugTracerConfig, GethDebugTracerType,
GethDebugTracingOptions, GethTrace, PreStateConfig,
};
use alloy::{
primitives::Address,
rpc::types::{TransactionRequest, trace::geth::DiffMode},
};
use anyhow::Context as _;
use futures::{TryStreamExt, future::try_join_all};
use indexmap::IndexMap;
use revive_dt_common::types::PlatformIdentifier;
use revive_dt_format::traits::{ResolutionContext, ResolverApi};
use revive_dt_report::ExecutionSpecificReporter;
use semver::Version;
use revive_dt_format::case::Case;
use revive_dt_format::input::{
BalanceAssertionStep, Calldata, EtherValue, Expected, ExpectedOutput, FunctionCallStep, Method,
StepIdx, StorageEmptyAssertionStep,
};
use revive_dt_format::metadata::{ContractIdent, ContractInstance, ContractPathAndIdent};
use revive_dt_format::{input::Step, metadata::Metadata};
use revive_dt_node_interaction::EthereumNode;
use tokio::try_join;
use tracing::{Instrument, info, info_span, instrument};
#[derive(Clone)]
pub struct CaseState {
/// A map of all of the compiled contracts for the given metadata file.
compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
/// This map stores the contracts deployments for this case.
deployed_contracts: HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>,
/// This map stores the variables used for each one of the cases contained in the metadata
/// file.
variables: HashMap<String, U256>,
/// Stores the version used for the current case.
compiler_version: Version,
/// The execution reporter.
execution_reporter: ExecutionSpecificReporter,
}
impl CaseState {
pub fn new(
compiler_version: Version,
compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
deployed_contracts: HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>,
execution_reporter: ExecutionSpecificReporter,
) -> Self {
Self {
compiled_contracts,
deployed_contracts,
variables: Default::default(),
compiler_version,
execution_reporter,
}
}
pub async fn handle_step(
&mut self,
metadata: &Metadata,
step: &Step,
node: &dyn EthereumNode,
) -> anyhow::Result<StepOutput> {
match step {
Step::FunctionCall(input) => {
let (receipt, geth_trace, diff_mode) = self
.handle_input(metadata, input, node)
.await
.context("Failed to handle function call step")?;
Ok(StepOutput::FunctionCall(receipt, geth_trace, diff_mode))
}
Step::BalanceAssertion(balance_assertion) => {
self.handle_balance_assertion(metadata, balance_assertion, node)
.await
.context("Failed to handle balance assertion step")?;
Ok(StepOutput::BalanceAssertion)
}
Step::StorageEmptyAssertion(storage_empty) => {
self.handle_storage_empty(metadata, storage_empty, node)
.await
.context("Failed to handle storage empty assertion step")?;
Ok(StepOutput::StorageEmptyAssertion)
}
Step::Repeat(repetition_step) => {
self.handle_repeat(
metadata,
repetition_step.repeat,
&repetition_step.steps,
node,
)
.await
.context("Failed to handle the repetition step")?;
Ok(StepOutput::Repetition)
}
}
.inspect(|_| info!("Step Succeeded"))
}
#[instrument(level = "info", name = "Handling Input", skip_all)]
pub async fn handle_input(
&mut self,
metadata: &Metadata,
input: &FunctionCallStep,
node: &dyn EthereumNode,
) -> anyhow::Result<(TransactionReceipt, GethTrace, DiffMode)> {
let resolver = node.resolver().await?;
let deployment_receipts = self
.handle_input_contract_deployment(metadata, input, node)
.await
.context("Failed during contract deployment phase of input handling")?;
let execution_receipt = self
.handle_input_execution(input, deployment_receipts, node)
.await
.context("Failed during transaction execution phase of input handling")?;
let tracing_result = self
.handle_input_call_frame_tracing(execution_receipt.transaction_hash, node)
.await
.context("Failed during callframe tracing phase of input handling")?;
self.handle_input_variable_assignment(input, &tracing_result)
.context("Failed to assign variables from callframe output")?;
let (_, (geth_trace, diff_mode)) = try_join!(
self.handle_input_expectations(
input,
&execution_receipt,
resolver.as_ref(),
&tracing_result
),
self.handle_input_diff(execution_receipt.transaction_hash, node)
)
.context("Failed while evaluating expectations and diffs in parallel")?;
Ok((execution_receipt, geth_trace, diff_mode))
}
#[instrument(level = "info", name = "Handling Balance Assertion", skip_all)]
pub async fn handle_balance_assertion(
&mut self,
metadata: &Metadata,
balance_assertion: &BalanceAssertionStep,
node: &dyn EthereumNode,
) -> anyhow::Result<()> {
self.handle_balance_assertion_contract_deployment(metadata, balance_assertion, node)
.await
.context("Failed to deploy contract for balance assertion")?;
self.handle_balance_assertion_execution(balance_assertion, node)
.await
.context("Failed to execute balance assertion")?;
Ok(())
}
#[instrument(level = "info", name = "Handling Storage Assertion", skip_all)]
pub async fn handle_storage_empty(
&mut self,
metadata: &Metadata,
storage_empty: &StorageEmptyAssertionStep,
node: &dyn EthereumNode,
) -> anyhow::Result<()> {
self.handle_storage_empty_assertion_contract_deployment(metadata, storage_empty, node)
.await
.context("Failed to deploy contract for storage empty assertion")?;
self.handle_storage_empty_assertion_execution(storage_empty, node)
.await
.context("Failed to execute storage empty assertion")?;
Ok(())
}
#[instrument(level = "info", name = "Handling Repetition", skip_all)]
pub async fn handle_repeat(
&mut self,
metadata: &Metadata,
repetitions: usize,
steps: &[Step],
node: &dyn EthereumNode,
) -> anyhow::Result<()> {
let tasks = (0..repetitions).map(|_| {
let mut state = self.clone();
async move {
for step in steps {
state.handle_step(metadata, step, node).await?;
}
Ok::<(), anyhow::Error>(())
}
});
try_join_all(tasks).await?;
Ok(())
}
/// 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(
&mut self,
metadata: &Metadata,
input: &FunctionCallStep,
node: &dyn EthereumNode,
) -> anyhow::Result<HashMap<ContractInstance, TransactionReceipt>> {
let mut instances_we_must_deploy = IndexMap::<ContractInstance, bool>::new();
for instance in input.find_all_contract_instances().into_iter() {
if !self.deployed_contracts.contains_key(&instance) {
instances_we_must_deploy.entry(instance).or_insert(false);
}
}
if let Method::Deployer = input.method {
instances_we_must_deploy.swap_remove(&input.instance);
instances_we_must_deploy.insert(input.instance.clone(), true);
}
let mut receipts = HashMap::new();
for (instance, deploy_with_constructor_arguments) in instances_we_must_deploy.into_iter() {
let calldata = deploy_with_constructor_arguments.then_some(&input.calldata);
let value = deploy_with_constructor_arguments
.then_some(input.value)
.flatten();
if let (_, _, Some(receipt)) = self
.get_or_deploy_contract_instance(
&instance,
metadata,
input.caller,
calldata,
value,
node,
)
.await
.context("Failed to get or deploy contract instance during input execution")?
{
receipts.insert(instance.clone(), receipt);
}
}
Ok(receipts)
}
/// 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(
&mut self,
input: &FunctionCallStep,
mut deployment_receipts: HashMap<ContractInstance, TransactionReceipt>,
node: &dyn EthereumNode,
) -> anyhow::Result<TransactionReceipt> {
match input.method {
// This input was already executed when `handle_input` was called. We just need to
// lookup the transaction receipt in this case and continue on.
Method::Deployer => deployment_receipts
.remove(&input.instance)
.context("Failed to find deployment receipt for constructor call"),
Method::Fallback | Method::FunctionName(_) => {
let resolver = node.resolver().await?;
let tx = match input
.legacy_transaction(resolver.as_ref(), self.default_resolution_context())
.await
{
Ok(tx) => tx,
Err(err) => {
return Err(err);
}
};
match node.execute_transaction(tx).await {
Ok(receipt) => Ok(receipt),
Err(err) => Err(err),
}
}
}
}
#[instrument(level = "info", skip_all)]
async fn handle_input_call_frame_tracing(
&self,
tx_hash: TxHash,
node: &dyn EthereumNode,
) -> anyhow::Result<CallFrame> {
node.trace_transaction(
tx_hash,
GethDebugTracingOptions {
tracer: Some(GethDebugTracerType::BuiltInTracer(
GethDebugBuiltInTracerType::CallTracer,
)),
tracer_config: GethDebugTracerConfig(serde_json::json! {{
"onlyTopCall": true,
"withLog": false,
"withStorage": false,
"withMemory": false,
"withStack": false,
"withReturnData": true
}}),
..Default::default()
},
)
.await
.map(|trace| {
trace
.try_into_call_frame()
.expect("Impossible - we requested a callframe trace so we must get it back")
})
}
#[instrument(level = "info", skip_all)]
fn handle_input_variable_assignment(
&mut self,
input: &FunctionCallStep,
tracing_result: &CallFrame,
) -> anyhow::Result<()> {
let Some(ref assignments) = input.variable_assignments else {
return Ok(());
};
// Handling the return data variable assignments.
for (variable_name, output_word) in assignments.return_data.iter().zip(
tracing_result
.output
.as_ref()
.unwrap_or_default()
.to_vec()
.chunks(32),
) {
let value = U256::from_be_slice(output_word);
self.variables.insert(variable_name.clone(), value);
tracing::info!(
variable_name,
variable_value = hex::encode(value.to_be_bytes::<32>()),
"Assigned variable"
);
}
Ok(())
}
#[instrument(level = "info", skip_all)]
async fn handle_input_expectations(
&self,
input: &FunctionCallStep,
execution_receipt: &TransactionReceipt,
resolver: &(impl ResolverApi + ?Sized),
tracing_result: &CallFrame,
) -> anyhow::Result<()> {
// Resolving the `input.expected` into a series of expectations that we can then assert on.
let mut expectations = match input {
FunctionCallStep {
expected: Some(Expected::Calldata(calldata)),
..
} => vec![ExpectedOutput::new().with_calldata(calldata.clone())],
FunctionCallStep {
expected: Some(Expected::Expected(expected)),
..
} => vec![expected.clone()],
FunctionCallStep {
expected: Some(Expected::ExpectedMany(expected)),
..
} => expected.clone(),
FunctionCallStep { expected: None, .. } => vec![ExpectedOutput::new().with_success()],
};
// This is a bit of a special case and we have to support it separately on it's own. If it's
// a call to the deployer method, then the tests will assert that it "returns" the address
// of the contract. Deployments do not return the address of the contract but the runtime
// code of the contracts. Therefore, this assertion would always fail. So, we replace it
// with an assertion of "check if it succeeded"
if let Method::Deployer = &input.method {
for expectation in expectations.iter_mut() {
expectation.return_data = None;
}
}
futures::stream::iter(expectations.into_iter().map(Ok))
.try_for_each_concurrent(None, |expectation| async move {
self.handle_input_expectation_item(
execution_receipt,
resolver,
expectation,
tracing_result,
)
.await
})
.await
}
#[instrument(level = "info", skip_all)]
async fn handle_input_expectation_item(
&self,
execution_receipt: &TransactionReceipt,
resolver: &(impl ResolverApi + ?Sized),
expectation: ExpectedOutput,
tracing_result: &CallFrame,
) -> anyhow::Result<()> {
if let Some(ref version_requirement) = expectation.compiler_version {
if !version_requirement.matches(&self.compiler_version) {
return Ok(());
}
}
let resolution_context = self
.default_resolution_context()
.with_block_number(execution_receipt.block_number.as_ref())
.with_transaction_hash(&execution_receipt.transaction_hash);
// Handling the receipt state assertion.
let expected = !expectation.exception;
let actual = execution_receipt.status();
if actual != expected {
tracing::error!(
expected,
actual,
?execution_receipt,
?tracing_result,
"Transaction status assertion failed"
);
anyhow::bail!(
"Transaction status assertion failed - Expected {expected} but got {actual}",
);
}
// Handling the calldata assertion
if let Some(ref expected_calldata) = expectation.return_data {
let expected = expected_calldata;
let actual = &tracing_result.output.as_ref().unwrap_or_default();
if !expected
.is_equivalent(actual, resolver, resolution_context)
.await
.context("Failed to resolve calldata equivalence for return data assertion")?
{
tracing::error!(
?execution_receipt,
?expected,
%actual,
"Calldata assertion failed"
);
anyhow::bail!("Calldata assertion failed - Expected {expected:?} but got {actual}",);
}
}
// Handling the events assertion
if let Some(ref expected_events) = expectation.events {
// Handling the events length assertion.
let expected = expected_events.len();
let actual = execution_receipt.logs().len();
if actual != expected {
tracing::error!(expected, actual, "Event count assertion failed",);
anyhow::bail!(
"Event count assertion failed - Expected {expected} but got {actual}",
);
}
// Handling the events assertion.
for (event_idx, (expected_event, actual_event)) in expected_events
.iter()
.zip(execution_receipt.logs())
.enumerate()
{
// Handling the emitter assertion.
if let Some(ref expected_address) = expected_event.address {
let expected = Address::from_slice(
Calldata::new_compound([expected_address])
.calldata(resolver, resolution_context)
.await?
.get(12..32)
.expect("Can't fail"),
);
let actual = actual_event.address();
if actual != expected {
tracing::error!(
event_idx,
%expected,
%actual,
"Event emitter assertion failed",
);
anyhow::bail!(
"Event emitter assertion failed - Expected {expected} but got {actual}",
);
}
}
// Handling the topics assertion.
for (expected, actual) in expected_event
.topics
.as_slice()
.iter()
.zip(actual_event.topics())
{
let expected = Calldata::new_compound([expected]);
if !expected
.is_equivalent(&actual.0, resolver, resolution_context)
.await
.context("Failed to resolve event topic equivalence")?
{
tracing::error!(
event_idx,
?execution_receipt,
?expected,
?actual,
"Event topics assertion failed",
);
anyhow::bail!(
"Event topics assertion failed - Expected {expected:?} but got {actual:?}",
);
}
}
// Handling the values assertion.
let expected = &expected_event.values;
let actual = &actual_event.data().data;
if !expected
.is_equivalent(&actual.0, resolver, resolution_context)
.await
.context("Failed to resolve event value equivalence")?
{
tracing::error!(
event_idx,
?execution_receipt,
?expected,
?actual,
"Event value assertion failed",
);
anyhow::bail!(
"Event value assertion failed - Expected {expected:?} but got {actual:?}",
);
}
}
}
Ok(())
}
#[instrument(level = "info", skip_all)]
async fn handle_input_diff(
&self,
tx_hash: TxHash,
node: &dyn EthereumNode,
) -> anyhow::Result<(GethTrace, DiffMode)> {
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
diff_mode: Some(true),
disable_code: None,
disable_storage: None,
});
let trace = node
.trace_transaction(tx_hash, trace_options)
.await
.context("Failed to obtain geth prestate tracer output")?;
let diff = node
.state_diff(tx_hash)
.await
.context("Failed to obtain state diff for transaction")?;
Ok((trace, diff))
}
#[instrument(level = "info", skip_all)]
pub async fn handle_balance_assertion_contract_deployment(
&mut self,
metadata: &Metadata,
balance_assertion: &BalanceAssertionStep,
node: &dyn EthereumNode,
) -> anyhow::Result<()> {
let Some(instance) = balance_assertion
.address
.strip_suffix(".address")
.map(ContractInstance::new)
else {
return Ok(());
};
self.get_or_deploy_contract_instance(
&instance,
metadata,
FunctionCallStep::default_caller(),
None,
None,
node,
)
.await?;
Ok(())
}
#[instrument(level = "info", skip_all)]
pub async fn handle_balance_assertion_execution(
&mut self,
BalanceAssertionStep {
address: address_string,
expected_balance: amount,
..
}: &BalanceAssertionStep,
node: &dyn EthereumNode,
) -> anyhow::Result<()> {
let resolver = node.resolver().await?;
let address = Address::from_slice(
Calldata::new_compound([address_string])
.calldata(resolver.as_ref(), self.default_resolution_context())
.await?
.get(12..32)
.expect("Can't fail"),
);
let balance = node.balance_of(address).await?;
let expected = *amount;
let actual = balance;
if expected != actual {
tracing::error!(%expected, %actual, %address, "Balance assertion failed");
anyhow::bail!(
"Balance assertion failed - Expected {} but got {} for {} resolved to {}",
expected,
actual,
address_string,
address,
)
}
Ok(())
}
#[instrument(level = "info", skip_all)]
pub async fn handle_storage_empty_assertion_contract_deployment(
&mut self,
metadata: &Metadata,
storage_empty_assertion: &StorageEmptyAssertionStep,
node: &dyn EthereumNode,
) -> anyhow::Result<()> {
let Some(instance) = storage_empty_assertion
.address
.strip_suffix(".address")
.map(ContractInstance::new)
else {
return Ok(());
};
self.get_or_deploy_contract_instance(
&instance,
metadata,
FunctionCallStep::default_caller(),
None,
None,
node,
)
.await?;
Ok(())
}
#[instrument(level = "info", skip_all)]
pub async fn handle_storage_empty_assertion_execution(
&mut self,
StorageEmptyAssertionStep {
address: address_string,
is_storage_empty,
..
}: &StorageEmptyAssertionStep,
node: &dyn EthereumNode,
) -> anyhow::Result<()> {
let resolver = node.resolver().await?;
let address = Address::from_slice(
Calldata::new_compound([address_string])
.calldata(resolver.as_ref(), self.default_resolution_context())
.await?
.get(12..32)
.expect("Can't fail"),
);
let storage = node.latest_state_proof(address, Default::default()).await?;
let is_empty = storage.storage_hash == EMPTY_ROOT_HASH;
let expected = is_storage_empty;
let actual = is_empty;
if *expected != actual {
tracing::error!(%expected, %actual, %address, "Storage Empty Assertion failed");
anyhow::bail!(
"Storage Empty Assertion failed - Expected {} but got {} for {} resolved to {}",
expected,
actual,
address_string,
address,
)
};
Ok(())
}
/// Gets the information of a deployed contract or library from the state. If it's found to not
/// be deployed then it will be deployed.
///
/// If a [`CaseIdx`] is not specified then this contact instance address will be stored in the
/// cross-case deployed contracts address mapping.
#[allow(clippy::too_many_arguments)]
pub async fn get_or_deploy_contract_instance(
&mut self,
contract_instance: &ContractInstance,
metadata: &Metadata,
deployer: Address,
calldata: Option<&Calldata>,
value: Option<EtherValue>,
node: &dyn EthereumNode,
) -> anyhow::Result<(Address, JsonAbi, Option<TransactionReceipt>)> {
if let Some((_, address, abi)) = self.deployed_contracts.get(contract_instance) {
return Ok((*address, abi.clone(), None));
}
let Some(ContractPathAndIdent {
contract_source_path,
contract_ident,
}) = metadata.contract_sources()?.remove(contract_instance)
else {
anyhow::bail!(
"Contract source not found for instance {:?}",
contract_instance
)
};
let Some((code, abi)) = self
.compiled_contracts
.get(&contract_source_path)
.and_then(|source_file_contracts| source_file_contracts.get(contract_ident.as_ref()))
.cloned()
else {
anyhow::bail!(
"Failed to find information for contract {:?}",
contract_instance
)
};
let mut code = match alloy::hex::decode(&code) {
Ok(code) => code,
Err(error) => {
tracing::error!(
?error,
contract_source_path = contract_source_path.display().to_string(),
contract_ident = contract_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)
}
};
if let Some(calldata) = calldata {
let resolver = node.resolver().await?;
let calldata = calldata
.calldata(resolver.as_ref(), self.default_resolution_context())
.await?;
code.extend(calldata);
}
let tx = {
let tx = TransactionRequest::default().from(deployer);
let tx = match value {
Some(ref value) => tx.value(value.into_inner()),
_ => tx,
};
TransactionBuilder::<Ethereum>::with_deploy_code(tx, code)
};
let receipt = match node.execute_transaction(tx).await {
Ok(receipt) => receipt,
Err(error) => {
tracing::error!(?error, "Contract deployment transaction failed.");
return Err(error);
}
};
let Some(address) = receipt.contract_address else {
anyhow::bail!("Contract deployment didn't return an address");
};
tracing::info!(
instance_name = ?contract_instance,
instance_address = ?address,
"Deployed contract"
);
self.execution_reporter
.report_contract_deployed_event(contract_instance.clone(), address)?;
self.deployed_contracts.insert(
contract_instance.clone(),
(contract_ident, address, abi.clone()),
);
Ok((address, abi, Some(receipt)))
}
fn default_resolution_context(&self) -> ResolutionContext<'_> {
ResolutionContext::default()
.with_deployed_contracts(&self.deployed_contracts)
.with_variables(&self.variables)
}
}
pub struct CaseDriver<'a> {
metadata: &'a Metadata,
case: &'a Case,
platform_state: Vec<(&'a dyn EthereumNode, PlatformIdentifier, CaseState)>,
}
impl<'a> CaseDriver<'a> {
#[allow(clippy::too_many_arguments)]
pub fn new(
metadata: &'a Metadata,
case: &'a Case,
platform_state: Vec<(&'a dyn EthereumNode, PlatformIdentifier, CaseState)>,
) -> CaseDriver<'a> {
Self {
metadata,
case,
platform_state,
}
}
#[instrument(level = "info", name = "Executing Case", skip_all)]
pub async fn execute(&mut self) -> anyhow::Result<usize> {
let mut steps_executed = 0;
for (step_idx, step) in self
.case
.steps_iterator()
.enumerate()
.map(|(idx, v)| (StepIdx::new(idx), v))
{
// Run this step concurrently across all platforms; short-circuit on first failure
let metadata = self.metadata;
let step_futs =
self.platform_state
.iter_mut()
.map(|(node, platform_id, case_state)| {
let platform_id = *platform_id;
let node_ref = *node;
let step_clone = step.clone();
let span = info_span!(
"Handling Step",
%step_idx,
platform = %platform_id,
);
async move {
case_state
.handle_step(metadata, &step_clone, node_ref)
.await
.map_err(|e| (platform_id, e))
}
.instrument(span)
});
match try_join_all(step_futs).await {
Ok(_outputs) => {
// All platforms succeeded for this step
steps_executed += 1;
}
Err((platform_id, error)) => {
tracing::error!(
%step_idx,
platform = %platform_id,
?error,
"Step failed on platform",
);
return Err(error);
}
}
}
Ok(steps_executed)
}
}
#[derive(Clone, Debug)]
#[allow(clippy::large_enum_variant)]
pub enum StepOutput {
FunctionCall(TransactionReceipt, GethTrace, DiffMode),
BalanceAssertion,
StorageEmptyAssertion,
Repetition,
}
@@ -5,7 +5,7 @@ use std::{
borrow::Cow,
collections::HashMap,
path::{Path, PathBuf},
sync::Arc,
sync::{Arc, LazyLock},
};
use futures::FutureExt;
@@ -19,7 +19,7 @@ use anyhow::{Context as _, Error, Result};
use revive_dt_report::ExecutionSpecificReporter;
use semver::Version;
use serde::{Deserialize, Serialize};
use tokio::sync::{Mutex, RwLock};
use tokio::sync::{Mutex, RwLock, Semaphore};
use tracing::{Instrument, debug, debug_span, instrument};
pub struct CachedCompiler<'a> {
@@ -165,10 +165,22 @@ impl<'a> CachedCompiler<'a> {
cache_value.compiler_output
}
None => {
compilation_callback()
let compiler_output = compilation_callback()
.await
.context("Compilation callback failed (cache miss path)")?
.compiler_output
.compiler_output;
self.artifacts_cache
.insert(
&cache_key,
&CacheValue {
compiler_output: compiler_output.clone(),
},
)
.await
.context(
"Failed to write the cached value of the compilation artifacts",
)?;
compiler_output
}
}
}
@@ -186,6 +198,12 @@ async fn compile_contracts(
compiler: &dyn SolidityCompiler,
reporter: &ExecutionSpecificReporter,
) -> Result<CompilerOutput> {
// Puts a limit on how many compilations we can perform at any given instance which helps us
// with some of the errors we've been seeing with high concurrency on MacOS (we have not tried
// it on Linux so we don't know if these issues also persist there or not.)
static SPAWN_GATE: LazyLock<Semaphore> = LazyLock::new(|| Semaphore::new(5));
let _permit = SPAWN_GATE.acquire().await?;
let all_sources_in_dir = FilesWithExtensionIterator::new(metadata_directory.as_ref())
.with_allowed_extension("sol")
.with_use_cached_fs(true)
+33
View File
@@ -0,0 +1,33 @@
use revive_dt_config::CorpusConfiguration;
use revive_dt_format::{corpus::Corpus, metadata::MetadataFile};
use tracing::{info, info_span, instrument};
/// Given an object that implements [`AsRef<CorpusConfiguration>`], this function finds all of the
/// corpus files and produces a map containing all of the [`MetadataFile`]s discovered.
#[instrument(level = "debug", name = "Collecting Corpora", skip_all)]
pub fn collect_metadata_files(
context: impl AsRef<CorpusConfiguration>,
) -> anyhow::Result<Vec<MetadataFile>> {
let mut metadata_files = Vec::new();
let corpus_configuration = AsRef::<CorpusConfiguration>::as_ref(&context);
for path in &corpus_configuration.paths {
let span = info_span!("Processing corpus file", path = %path.display());
let _guard = span.enter();
let corpus = Corpus::try_from_path(path)?;
info!(
name = corpus.name(),
number_of_contained_paths = corpus.path_count(),
"Deserialized corpus file"
);
metadata_files.extend(corpus.enumerate_tests());
}
// There's a possibility that there are certain paths that all lead to the same metadata files
// and therefore it's important that we sort them and then deduplicate them.
metadata_files.sort_by(|a, b| a.metadata_file_path.cmp(&b.metadata_file_path));
metadata_files.dedup_by(|a, b| a.metadata_file_path == b.metadata_file_path);
Ok(metadata_files)
}
+9
View File
@@ -0,0 +1,9 @@
mod cached_compiler;
mod metadata;
mod pool;
mod test;
pub use cached_compiler::*;
pub use metadata::*;
pub use pool::*;
pub use test::*;
@@ -16,7 +16,7 @@ pub struct NodePool {
impl NodePool {
/// Create a new Pool. This will start as many nodes as there are workers in `config`.
pub fn new(context: Context, platform: &dyn Platform) -> anyhow::Result<Self> {
pub async fn new(context: Context, platform: &dyn Platform) -> anyhow::Result<Self> {
let concurrency_configuration = AsRef::<ConcurrencyConfiguration>::as_ref(&context);
let nodes = concurrency_configuration.number_of_nodes;
@@ -33,11 +33,18 @@ impl NodePool {
.join()
.map_err(|error| anyhow::anyhow!("failed to spawn node: {:?}", error))
.context("Failed to join node spawn thread")?
.map_err(|error| anyhow::anyhow!("node failed to spawn: {error}"))
.context("Node failed to spawn")?,
);
}
let pre_transactions_tasks = nodes
.iter_mut()
.map(|node| node.pre_transactions())
.collect::<Vec<_>>();
futures::future::try_join_all(pre_transactions_tasks)
.await
.context("Failed to run the pre-transactions task")?;
Ok(Self {
nodes,
next: Default::default(),
+325
View File
@@ -0,0 +1,325 @@
use std::collections::BTreeMap;
use std::sync::Arc;
use std::{borrow::Cow, path::Path};
use futures::{Stream, StreamExt, stream};
use indexmap::{IndexMap, indexmap};
use revive_dt_common::iterators::EitherIter;
use revive_dt_common::types::PlatformIdentifier;
use revive_dt_config::Context;
use revive_dt_format::mode::ParsedMode;
use serde_json::{Value, json};
use revive_dt_compiler::Mode;
use revive_dt_compiler::SolidityCompiler;
use revive_dt_format::{
case::{Case, CaseIdx},
metadata::MetadataFile,
};
use revive_dt_node_interaction::EthereumNode;
use revive_dt_report::{ExecutionSpecificReporter, Reporter};
use revive_dt_report::{TestSpecificReporter, TestSpecifier};
use tracing::{debug, error, info};
use crate::Platform;
use crate::helpers::NodePool;
pub async fn create_test_definitions_stream<'a>(
// This is only required for creating the compiler objects and is not used anywhere else in the
// function.
context: &Context,
metadata_files: impl IntoIterator<Item = &'a MetadataFile>,
platforms_and_nodes: &'a BTreeMap<PlatformIdentifier, (&dyn Platform, NodePool)>,
reporter: Reporter,
) -> impl Stream<Item = TestDefinition<'a>> {
stream::iter(
metadata_files
.into_iter()
// Flatten over the cases.
.flat_map(|metadata_file| {
metadata_file
.cases
.iter()
.enumerate()
.map(move |(case_idx, case)| (metadata_file, case_idx, case))
})
// Flatten over the modes, prefer the case modes over the metadata file modes.
.flat_map(move |(metadata_file, case_idx, case)| {
let reporter = reporter.clone();
let modes = case.modes.as_ref().or(metadata_file.modes.as_ref());
let modes = match modes {
Some(modes) => EitherIter::A(
ParsedMode::many_to_modes(modes.iter()).map(Cow::<'static, _>::Owned),
),
None => EitherIter::B(Mode::all().map(Cow::<'static, _>::Borrowed)),
};
modes.into_iter().map(move |mode| {
(
metadata_file,
case_idx,
case,
mode.clone(),
reporter.test_specific_reporter(Arc::new(TestSpecifier {
solc_mode: mode.as_ref().clone(),
metadata_file_path: metadata_file.metadata_file_path.clone(),
case_idx: CaseIdx::new(case_idx),
})),
)
})
})
// Inform the reporter of each one of the test cases that were discovered which we expect to
// run.
.inspect(|(_, _, _, _, reporter)| {
reporter
.report_test_case_discovery_event()
.expect("Can't fail");
}),
)
// Creating the Test Definition objects from all of the various objects we have and creating
// their required dependencies (e.g., compiler).
.filter_map(
move |(metadata_file, case_idx, case, mode, reporter)| async move {
let mut platforms = BTreeMap::new();
for (platform, node_pool) in platforms_and_nodes.values() {
let node = node_pool.round_robbin();
let compiler = platform
.new_compiler(context.clone(), mode.version.clone().map(Into::into))
.await
.inspect_err(|err| {
error!(
?err,
platform_identifier = %platform.platform_identifier(),
"Failed to instantiate the compiler"
)
})
.ok()?;
reporter
.report_node_assigned_event(
node.id(),
platform.platform_identifier(),
node.connection_string(),
)
.expect("Can't fail");
let reporter =
reporter.execution_specific_reporter(node.id(), platform.platform_identifier());
platforms.insert(
platform.platform_identifier(),
TestPlatformInformation {
platform: *platform,
node,
compiler,
reporter,
},
);
}
Some(TestDefinition {
/* Metadata file information */
metadata: metadata_file,
metadata_file_path: metadata_file.metadata_file_path.as_path(),
/* Mode Information */
mode: mode.clone(),
/* Case Information */
case_idx: CaseIdx::new(case_idx),
case,
/* Platform and Node Assignment Information */
platforms,
/* Reporter */
reporter,
})
},
)
// Filter out the test cases which are incompatible or that can't run in the current setup.
.filter_map(move |test| async move {
match test.check_compatibility() {
Ok(()) => Some(test),
Err((reason, additional_information)) => {
debug!(
metadata_file_path = %test.metadata.metadata_file_path.display(),
case_idx = %test.case_idx,
mode = %test.mode,
reason,
additional_information =
serde_json::to_string(&additional_information).unwrap(),
"Ignoring Test Case"
);
test.reporter
.report_test_ignored_event(
reason.to_string(),
additional_information
.into_iter()
.map(|(k, v)| (k.into(), v))
.collect::<IndexMap<_, _>>(),
)
.expect("Can't fail");
None
}
}
})
.inspect(|test| {
info!(
metadata_file_path = %test.metadata_file_path.display(),
case_idx = %test.case_idx,
mode = %test.mode,
"Created a test case definition"
);
})
}
/// This is a full description of a differential test to run alongside the full metadata file, the
/// specific case to be tested, the platforms that the tests should run on, the specific nodes of
/// these platforms that they should run on, the compilers to use, and everything else needed making
/// it a complete description.
pub struct TestDefinition<'a> {
/* Metadata file information */
pub metadata: &'a MetadataFile,
pub metadata_file_path: &'a Path,
/* Mode Information */
pub mode: Cow<'a, Mode>,
/* Case Information */
pub case_idx: CaseIdx,
pub case: &'a Case,
/* Platform and Node Assignment Information */
pub platforms: BTreeMap<PlatformIdentifier, TestPlatformInformation<'a>>,
/* Reporter */
pub reporter: TestSpecificReporter,
}
impl<'a> TestDefinition<'a> {
/// Checks if this test can be ran with the current configuration.
pub fn check_compatibility(&self) -> TestCheckFunctionResult {
self.check_metadata_file_ignored()?;
self.check_case_file_ignored()?;
self.check_target_compatibility()?;
self.check_evm_version_compatibility()?;
self.check_compiler_compatibility()?;
Ok(())
}
/// Checks if the metadata file is ignored or not.
fn check_metadata_file_ignored(&self) -> TestCheckFunctionResult {
if self.metadata.ignore.is_some_and(|ignore| ignore) {
Err(("Metadata file is ignored.", indexmap! {}))
} else {
Ok(())
}
}
/// Checks if the case file is ignored or not.
fn check_case_file_ignored(&self) -> TestCheckFunctionResult {
if self.case.ignore.is_some_and(|ignore| ignore) {
Err(("Case is ignored.", indexmap! {}))
} else {
Ok(())
}
}
/// Checks if the platforms all support the desired targets in the metadata file.
fn check_target_compatibility(&self) -> TestCheckFunctionResult {
let mut error_map = indexmap! {
"test_desired_targets" => json!(self.metadata.targets.as_ref()),
};
let mut is_allowed = true;
for (_, platform_information) in self.platforms.iter() {
let is_allowed_for_platform = match self.metadata.targets.as_ref() {
None => true,
Some(required_vm_identifiers) => {
required_vm_identifiers.contains(&platform_information.platform.vm_identifier())
}
};
is_allowed &= is_allowed_for_platform;
error_map.insert(
platform_information.platform.platform_identifier().into(),
json!(is_allowed_for_platform),
);
}
if is_allowed {
Ok(())
} else {
Err((
"One of the platforms do do not support the targets allowed by the test.",
error_map,
))
}
}
// Checks for the compatibility of the EVM version with the platforms specified.
fn check_evm_version_compatibility(&self) -> TestCheckFunctionResult {
let Some(evm_version_requirement) = self.metadata.required_evm_version else {
return Ok(());
};
let mut error_map = indexmap! {
"test_desired_evm_version" => json!(self.metadata.required_evm_version),
};
let mut is_allowed = true;
for (_, platform_information) in self.platforms.iter() {
let is_allowed_for_platform =
evm_version_requirement.matches(&platform_information.node.evm_version());
is_allowed &= is_allowed_for_platform;
error_map.insert(
platform_information.platform.platform_identifier().into(),
json!(is_allowed_for_platform),
);
}
if is_allowed {
Ok(())
} else {
Err((
"EVM version is incompatible for the platforms specified",
error_map,
))
}
}
/// Checks if the platforms compilers support the mode that the test is for.
fn check_compiler_compatibility(&self) -> TestCheckFunctionResult {
let mut error_map = indexmap! {
"test_desired_evm_version" => json!(self.metadata.required_evm_version),
};
let mut is_allowed = true;
for (_, platform_information) in self.platforms.iter() {
let is_allowed_for_platform = platform_information
.compiler
.supports_mode(self.mode.optimize_setting, self.mode.pipeline);
is_allowed &= is_allowed_for_platform;
error_map.insert(
platform_information.platform.platform_identifier().into(),
json!(is_allowed_for_platform),
);
}
if is_allowed {
Ok(())
} else {
Err((
"Compilers do not support this mode either for the provided platforms.",
error_map,
))
}
}
}
pub struct TestPlatformInformation<'a> {
pub platform: &'a dyn Platform,
pub node: &'a dyn EthereumNode,
pub compiler: Box<dyn SolidityCompiler>,
pub reporter: ExecutionSpecificReporter,
}
type TestCheckFunctionResult = Result<(), (&'static str, IndexMap<&'static str, Value>)>;
+174 -9
View File
@@ -13,12 +13,14 @@ use anyhow::Context as _;
use revive_dt_common::types::*;
use revive_dt_compiler::{SolidityCompiler, revive_resolc::Resolc, solc::Solc};
use revive_dt_config::*;
use revive_dt_node::{Node, geth::GethNode, substrate::SubstrateNode};
use revive_dt_node::{
Node, node_implementations::geth::GethNode,
node_implementations::lighthouse_geth::LighthouseGethNode,
node_implementations::substrate::SubstrateNode, node_implementations::zombienet::ZombienetNode,
};
use revive_dt_node_interaction::EthereumNode;
use tracing::info;
pub mod driver;
/// A trait that describes the interface for the platforms that are supported by the tool.
#[allow(clippy::type_complexity)]
pub trait Platform {
@@ -104,6 +106,51 @@ impl Platform for GethEvmSolcPlatform {
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
pub struct LighthouseGethEvmSolcPlatform;
impl Platform for LighthouseGethEvmSolcPlatform {
fn platform_identifier(&self) -> PlatformIdentifier {
PlatformIdentifier::LighthouseGethEvmSolc
}
fn node_identifier(&self) -> NodeIdentifier {
NodeIdentifier::LighthouseGeth
}
fn vm_identifier(&self) -> VmIdentifier {
VmIdentifier::Evm
}
fn compiler_identifier(&self) -> CompilerIdentifier {
CompilerIdentifier::Solc
}
fn new_node(
&self,
context: Context,
) -> anyhow::Result<JoinHandle<anyhow::Result<Box<dyn EthereumNode + Send + Sync>>>> {
let genesis_configuration = AsRef::<GenesisConfiguration>::as_ref(&context);
let genesis = genesis_configuration.genesis()?.clone();
Ok(thread::spawn(move || {
let node = LighthouseGethNode::new(context);
let node = spawn_node::<LighthouseGethNode>(node, genesis)?;
Ok(Box::new(node) as Box<_>)
}))
}
fn new_compiler(
&self,
context: Context,
version: Option<VersionOrRequirement>,
) -> Pin<Box<dyn Future<Output = anyhow::Result<Box<dyn SolidityCompiler>>>>> {
Box::pin(async move {
let compiler = Solc::new(context, version).await;
compiler.map(|compiler| Box::new(compiler) as Box<dyn SolidityCompiler>)
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
pub struct KitchensinkPolkavmResolcPlatform;
@@ -137,6 +184,7 @@ impl Platform for KitchensinkPolkavmResolcPlatform {
let node = SubstrateNode::new(
kitchensink_path,
SubstrateNode::KITCHENSINK_EXPORT_CHAINSPEC_COMMAND,
None,
context,
);
let node = spawn_node(node, genesis)?;
@@ -189,6 +237,7 @@ impl Platform for KitchensinkRevmSolcPlatform {
let node = SubstrateNode::new(
kitchensink_path,
SubstrateNode::KITCHENSINK_EXPORT_CHAINSPEC_COMMAND,
None,
context,
);
let node = spawn_node(node, genesis)?;
@@ -233,14 +282,17 @@ impl Platform for ReviveDevNodePolkavmResolcPlatform {
context: Context,
) -> anyhow::Result<JoinHandle<anyhow::Result<Box<dyn EthereumNode + Send + Sync>>>> {
let genesis_configuration = AsRef::<GenesisConfiguration>::as_ref(&context);
let revive_dev_node_path = AsRef::<ReviveDevNodeConfiguration>::as_ref(&context)
.path
.clone();
let revive_dev_node_configuration = AsRef::<ReviveDevNodeConfiguration>::as_ref(&context);
let revive_dev_node_path = revive_dev_node_configuration.path.clone();
let revive_dev_node_consensus = revive_dev_node_configuration.consensus.clone();
let genesis = genesis_configuration.genesis()?.clone();
Ok(thread::spawn(move || {
let node = SubstrateNode::new(
revive_dev_node_path,
SubstrateNode::REVIVE_DEV_NODE_EXPORT_CHAINSPEC_COMMAND,
Some(revive_dev_node_consensus),
context,
);
let node = spawn_node(node, genesis)?;
@@ -285,14 +337,17 @@ impl Platform for ReviveDevNodeRevmSolcPlatform {
context: Context,
) -> anyhow::Result<JoinHandle<anyhow::Result<Box<dyn EthereumNode + Send + Sync>>>> {
let genesis_configuration = AsRef::<GenesisConfiguration>::as_ref(&context);
let revive_dev_node_path = AsRef::<ReviveDevNodeConfiguration>::as_ref(&context)
.path
.clone();
let revive_dev_node_configuration = AsRef::<ReviveDevNodeConfiguration>::as_ref(&context);
let revive_dev_node_path = revive_dev_node_configuration.path.clone();
let revive_dev_node_consensus = revive_dev_node_configuration.consensus.clone();
let genesis = genesis_configuration.genesis()?.clone();
Ok(thread::spawn(move || {
let node = SubstrateNode::new(
revive_dev_node_path,
SubstrateNode::REVIVE_DEV_NODE_EXPORT_CHAINSPEC_COMMAND,
Some(revive_dev_node_consensus),
context,
);
let node = spawn_node(node, genesis)?;
@@ -312,10 +367,109 @@ impl Platform for ReviveDevNodeRevmSolcPlatform {
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
pub struct ZombienetPolkavmResolcPlatform;
impl Platform for ZombienetPolkavmResolcPlatform {
fn platform_identifier(&self) -> PlatformIdentifier {
PlatformIdentifier::ZombienetPolkavmResolc
}
fn node_identifier(&self) -> NodeIdentifier {
NodeIdentifier::Zombienet
}
fn vm_identifier(&self) -> VmIdentifier {
VmIdentifier::PolkaVM
}
fn compiler_identifier(&self) -> CompilerIdentifier {
CompilerIdentifier::Resolc
}
fn new_node(
&self,
context: Context,
) -> anyhow::Result<JoinHandle<anyhow::Result<Box<dyn EthereumNode + Send + Sync>>>> {
let genesis_configuration = AsRef::<GenesisConfiguration>::as_ref(&context);
let polkadot_parachain_path = AsRef::<PolkadotParachainConfiguration>::as_ref(&context)
.path
.clone();
let genesis = genesis_configuration.genesis()?.clone();
Ok(thread::spawn(move || {
let node = ZombienetNode::new(polkadot_parachain_path, context);
let node = spawn_node(node, genesis)?;
Ok(Box::new(node) as Box<_>)
}))
}
fn new_compiler(
&self,
context: Context,
version: Option<VersionOrRequirement>,
) -> Pin<Box<dyn Future<Output = anyhow::Result<Box<dyn SolidityCompiler>>>>> {
Box::pin(async move {
let compiler = Resolc::new(context, version).await;
compiler.map(|compiler| Box::new(compiler) as Box<dyn SolidityCompiler>)
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
pub struct ZombienetRevmSolcPlatform;
impl Platform for ZombienetRevmSolcPlatform {
fn platform_identifier(&self) -> PlatformIdentifier {
PlatformIdentifier::ZombienetRevmSolc
}
fn node_identifier(&self) -> NodeIdentifier {
NodeIdentifier::Zombienet
}
fn vm_identifier(&self) -> VmIdentifier {
VmIdentifier::Evm
}
fn compiler_identifier(&self) -> CompilerIdentifier {
CompilerIdentifier::Solc
}
fn new_node(
&self,
context: Context,
) -> anyhow::Result<JoinHandle<anyhow::Result<Box<dyn EthereumNode + Send + Sync>>>> {
let genesis_configuration = AsRef::<GenesisConfiguration>::as_ref(&context);
let polkadot_parachain_path = AsRef::<PolkadotParachainConfiguration>::as_ref(&context)
.path
.clone();
let genesis = genesis_configuration.genesis()?.clone();
Ok(thread::spawn(move || {
let node = ZombienetNode::new(polkadot_parachain_path, context);
let node = spawn_node(node, genesis)?;
Ok(Box::new(node) as Box<_>)
}))
}
fn new_compiler(
&self,
context: Context,
version: Option<VersionOrRequirement>,
) -> Pin<Box<dyn Future<Output = anyhow::Result<Box<dyn SolidityCompiler>>>>> {
Box::pin(async move {
let compiler = Solc::new(context, version).await;
compiler.map(|compiler| Box::new(compiler) as Box<dyn SolidityCompiler>)
})
}
}
impl From<PlatformIdentifier> for Box<dyn Platform> {
fn from(value: PlatformIdentifier) -> Self {
match value {
PlatformIdentifier::GethEvmSolc => Box::new(GethEvmSolcPlatform) as Box<_>,
PlatformIdentifier::LighthouseGethEvmSolc => {
Box::new(LighthouseGethEvmSolcPlatform) as Box<_>
}
PlatformIdentifier::KitchensinkPolkavmResolc => {
Box::new(KitchensinkPolkavmResolcPlatform) as Box<_>
}
@@ -328,6 +482,10 @@ impl From<PlatformIdentifier> for Box<dyn Platform> {
PlatformIdentifier::ReviveDevNodeRevmSolc => {
Box::new(ReviveDevNodeRevmSolcPlatform) as Box<_>
}
PlatformIdentifier::ZombienetPolkavmResolc => {
Box::new(ZombienetPolkavmResolcPlatform) as Box<_>
}
PlatformIdentifier::ZombienetRevmSolc => Box::new(ZombienetRevmSolcPlatform) as Box<_>,
}
}
}
@@ -336,6 +494,9 @@ impl From<PlatformIdentifier> for &dyn Platform {
fn from(value: PlatformIdentifier) -> Self {
match value {
PlatformIdentifier::GethEvmSolc => &GethEvmSolcPlatform as &dyn Platform,
PlatformIdentifier::LighthouseGethEvmSolc => {
&LighthouseGethEvmSolcPlatform as &dyn Platform
}
PlatformIdentifier::KitchensinkPolkavmResolc => {
&KitchensinkPolkavmResolcPlatform as &dyn Platform
}
@@ -348,6 +509,10 @@ impl From<PlatformIdentifier> for &dyn Platform {
PlatformIdentifier::ReviveDevNodeRevmSolc => {
&ReviveDevNodeRevmSolcPlatform as &dyn Platform
}
PlatformIdentifier::ZombienetPolkavmResolc => {
&ZombienetPolkavmResolcPlatform as &dyn Platform
}
PlatformIdentifier::ZombienetRevmSolc => &ZombienetRevmSolcPlatform as &dyn Platform,
}
}
}
+42 -731
View File
@@ -1,51 +1,21 @@
mod cached_compiler;
mod pool;
mod differential_benchmarks;
mod differential_tests;
mod helpers;
use std::{
borrow::Cow,
collections::{BTreeSet, HashMap},
io::{BufWriter, Write, stderr},
path::Path,
sync::Arc,
time::Instant,
};
use alloy::{
network::{Ethereum, TransactionBuilder},
rpc::types::TransactionRequest,
};
use anyhow::Context as _;
use clap::Parser;
use futures::stream;
use futures::{Stream, StreamExt};
use indexmap::{IndexMap, indexmap};
use revive_dt_node_interaction::EthereumNode;
use revive_dt_report::{
ExecutionSpecificReporter, ReportAggregator, Reporter, ReporterEvent, TestCaseStatus,
TestSpecificReporter, TestSpecifier,
};
use revive_dt_report::ReportAggregator;
use schemars::schema_for;
use serde_json::{Value, json};
use tracing::{debug, error, info, info_span, instrument};
use tracing::info;
use tracing_subscriber::{EnvFilter, FmtSubscriber};
use revive_dt_common::{iterators::EitherIter, types::Mode};
use revive_dt_compiler::SolidityCompiler;
use revive_dt_config::{Context, *};
use revive_dt_core::{
Platform,
driver::{CaseDriver, CaseState},
};
use revive_dt_format::{
case::{Case, CaseIdx},
corpus::Corpus,
input::{FunctionCallStep, Step},
metadata::{ContractPathAndIdent, Metadata, MetadataFile},
mode::ParsedMode,
};
use revive_dt_config::Context;
use revive_dt_core::Platform;
use revive_dt_format::metadata::Metadata;
use crate::cached_compiler::CachedCompiler;
use crate::pool::NodePool;
use crate::{
differential_benchmarks::handle_differential_benchmarks,
differential_tests::handle_differential_tests,
};
fn main() -> anyhow::Result<()> {
let (writer, _guard) = tracing_appender::non_blocking::NonBlockingBuilder::default()
@@ -71,37 +41,37 @@ fn main() -> anyhow::Result<()> {
let (reporter, report_aggregator_task) = ReportAggregator::new(context.clone()).into_task();
match context {
Context::ExecuteTests(context) => {
let tests = collect_corpora(&context)
.context("Failed to collect corpus files from provided arguments")?
.into_iter()
.inspect(|(corpus, _)| {
reporter
.report_corpus_file_discovery_event(corpus.clone())
.expect("Can't fail")
})
.flat_map(|(_, files)| files.into_iter())
.inspect(|metadata_file| {
reporter
.report_metadata_file_discovery_event(
metadata_file.metadata_file_path.clone(),
metadata_file.content.clone(),
)
.expect("Can't fail")
})
.collect::<Vec<_>>();
Context::Test(context) => tokio::runtime::Builder::new_multi_thread()
.worker_threads(context.concurrency_configuration.number_of_threads)
.enable_all()
.build()
.expect("Failed building the Runtime")
.block_on(async move {
let differential_tests_handling_task =
handle_differential_tests(*context, reporter);
tokio::runtime::Builder::new_multi_thread()
.worker_threads(context.concurrency_configuration.number_of_threads)
.enable_all()
.build()
.expect("Failed building the Runtime")
.block_on(async move {
execute_corpus(*context, &tests, reporter, report_aggregator_task)
.await
.context("Failed to execute corpus")
})
}
futures::future::try_join(differential_tests_handling_task, report_aggregator_task)
.await?;
Ok(())
}),
Context::Benchmark(context) => tokio::runtime::Builder::new_multi_thread()
.worker_threads(context.concurrency_configuration.number_of_threads)
.enable_all()
.build()
.expect("Failed building the Runtime")
.block_on(async move {
let differential_benchmarks_handling_task =
handle_differential_benchmarks(*context, reporter);
futures::future::try_join(
differential_benchmarks_handling_task,
report_aggregator_task,
)
.await?;
Ok(())
}),
Context::ExportJsonSchema => {
let schema = schema_for!(Metadata);
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
@@ -109,662 +79,3 @@ fn main() -> anyhow::Result<()> {
}
}
}
#[instrument(level = "debug", name = "Collecting Corpora", skip_all)]
fn collect_corpora(
context: &TestExecutionContext,
) -> anyhow::Result<HashMap<Corpus, Vec<MetadataFile>>> {
let mut corpora = HashMap::new();
for path in &context.corpus {
let span = info_span!("Processing corpus file", path = %path.display());
let _guard = span.enter();
let corpus = Corpus::try_from_path(path)?;
info!(
name = corpus.name(),
number_of_contained_paths = corpus.path_count(),
"Deserialized corpus file"
);
let tests = corpus.enumerate_tests();
corpora.insert(corpus, tests);
}
Ok(corpora)
}
async fn run_driver(
context: TestExecutionContext,
metadata_files: &[MetadataFile],
reporter: Reporter,
report_aggregator_task: impl Future<Output = anyhow::Result<()>>,
platforms: Vec<&dyn Platform>,
) -> anyhow::Result<()> {
let mut nodes = Vec::<(&dyn Platform, NodePool)>::new();
for platform in platforms.into_iter() {
let pool = NodePool::new(Context::ExecuteTests(Box::new(context.clone())), platform)
.inspect_err(|err| {
error!(
?err,
platform_identifier = %platform.platform_identifier(),
"Failed to initialize the node pool for the platform."
)
})
.context("Failed to initialize the node pool")?;
nodes.push((platform, pool));
}
let tests_stream = tests_stream(
&context,
metadata_files.iter(),
nodes.as_slice(),
reporter.clone(),
)
.await;
let driver_task = start_driver_task(&context, tests_stream)
.await
.context("Failed to start driver task")?;
let cli_reporting_task = start_cli_reporting_task(reporter);
let (_, _, rtn) = tokio::join!(cli_reporting_task, driver_task, report_aggregator_task);
rtn?;
Ok(())
}
async fn tests_stream<'a>(
args: &TestExecutionContext,
metadata_files: impl IntoIterator<Item = &'a MetadataFile> + Clone,
nodes: &'a [(&dyn Platform, NodePool)],
reporter: Reporter,
) -> impl Stream<Item = Test<'a>> {
let tests = metadata_files
.into_iter()
.flat_map(|metadata_file| {
metadata_file
.cases
.iter()
.enumerate()
.map(move |(case_idx, case)| (metadata_file, case_idx, case))
})
// Flatten over the modes, prefer the case modes over the metadata file modes.
.flat_map(|(metadata_file, case_idx, case)| {
let reporter = reporter.clone();
let modes = case.modes.as_ref().or(metadata_file.modes.as_ref());
let modes = match modes {
Some(modes) => EitherIter::A(
ParsedMode::many_to_modes(modes.iter()).map(Cow::<'static, _>::Owned),
),
None => EitherIter::B(Mode::all().map(Cow::<'static, _>::Borrowed)),
};
modes.into_iter().map(move |mode| {
(
metadata_file,
case_idx,
case,
mode.clone(),
reporter.test_specific_reporter(Arc::new(TestSpecifier {
solc_mode: mode.as_ref().clone(),
metadata_file_path: metadata_file.metadata_file_path.clone(),
case_idx: CaseIdx::new(case_idx),
})),
)
})
})
.collect::<Vec<_>>();
// Note: before we do any kind of filtering or process the iterator in any way, we need to
// inform the report aggregator of all of the cases that were found as it keeps a state of the
// test cases for its internal use.
for (_, _, _, _, reporter) in tests.iter() {
reporter
.report_test_case_discovery_event()
.expect("Can't fail")
}
stream::iter(tests.into_iter())
.filter_map(
move |(metadata_file, case_idx, case, mode, reporter)| async move {
let mut platforms = Vec::new();
for (platform, node_pool) in nodes.iter() {
let node = node_pool.round_robbin();
let compiler = platform
.new_compiler(
Context::ExecuteTests(Box::new(args.clone())),
mode.version.clone().map(Into::into),
)
.await
.inspect_err(|err| {
error!(
?err,
platform_identifier = %platform.platform_identifier(),
"Failed to instantiate the compiler"
)
})
.ok()?;
let reporter = reporter
.execution_specific_reporter(node.id(), platform.platform_identifier());
platforms.push((*platform, node, compiler, reporter));
}
Some(Test {
metadata: metadata_file,
metadata_file_path: metadata_file.metadata_file_path.as_path(),
mode: mode.clone(),
case_idx: CaseIdx::new(case_idx),
case,
platforms,
reporter,
})
},
)
.filter_map(move |test| async move {
match test.check_compatibility() {
Ok(()) => Some(test),
Err((reason, additional_information)) => {
debug!(
metadata_file_path = %test.metadata.metadata_file_path.display(),
case_idx = %test.case_idx,
mode = %test.mode,
reason,
additional_information =
serde_json::to_string(&additional_information).unwrap(),
"Ignoring Test Case"
);
test.reporter
.report_test_ignored_event(
reason.to_string(),
additional_information
.into_iter()
.map(|(k, v)| (k.into(), v))
.collect::<IndexMap<_, _>>(),
)
.expect("Can't fail");
None
}
}
})
}
async fn start_driver_task<'a>(
context: &TestExecutionContext,
tests: impl Stream<Item = Test<'a>>,
) -> anyhow::Result<impl Future<Output = ()>> {
info!("Starting driver task");
let cached_compiler = Arc::new(
CachedCompiler::new(
context
.working_directory
.as_path()
.join("compilation_cache"),
context
.compilation_configuration
.invalidate_compilation_cache,
)
.await
.context("Failed to initialize cached compiler")?,
);
Ok(tests.for_each_concurrent(
context.concurrency_configuration.concurrency_limit(),
move |test| {
let cached_compiler = cached_compiler.clone();
async move {
for (platform, node, _, _) in test.platforms.iter() {
test.reporter
.report_node_assigned_event(
node.id(),
platform.platform_identifier(),
node.connection_string(),
)
.expect("Can't fail");
}
let reporter = test.reporter.clone();
let result = handle_case_driver(&test, cached_compiler).await;
match result {
Ok(steps_executed) => reporter
.report_test_succeeded_event(steps_executed)
.expect("Can't fail"),
Err(error) => reporter
.report_test_failed_event(format!("{error:#}"))
.expect("Can't fail"),
}
}
},
))
}
#[allow(irrefutable_let_patterns, clippy::uninlined_format_args)]
async fn start_cli_reporting_task(reporter: Reporter) {
let mut aggregator_events_rx = reporter.subscribe().await.expect("Can't fail");
drop(reporter);
let start = Instant::now();
const GREEN: &str = "\x1B[32m";
const RED: &str = "\x1B[31m";
const GREY: &str = "\x1B[90m";
const COLOR_RESET: &str = "\x1B[0m";
const BOLD: &str = "\x1B[1m";
const BOLD_RESET: &str = "\x1B[22m";
let mut number_of_successes = 0;
let mut number_of_failures = 0;
let mut buf = BufWriter::new(stderr());
while let Ok(event) = aggregator_events_rx.recv().await {
let ReporterEvent::MetadataFileSolcModeCombinationExecutionCompleted {
metadata_file_path,
mode,
case_status,
} = event
else {
continue;
};
let _ = writeln!(buf, "{} - {}", mode, metadata_file_path.display());
for (case_idx, case_status) in case_status.into_iter() {
let _ = write!(buf, "\tCase Index {case_idx:>3}: ");
let _ = match case_status {
TestCaseStatus::Succeeded { steps_executed } => {
number_of_successes += 1;
writeln!(
buf,
"{}{}Case Succeeded{} - Steps Executed: {}{}",
GREEN, BOLD, BOLD_RESET, steps_executed, COLOR_RESET
)
}
TestCaseStatus::Failed { reason } => {
number_of_failures += 1;
writeln!(
buf,
"{}{}Case Failed{} - Reason: {}{}",
RED,
BOLD,
BOLD_RESET,
reason.trim(),
COLOR_RESET,
)
}
TestCaseStatus::Ignored { reason, .. } => writeln!(
buf,
"{}{}Case Ignored{} - Reason: {}{}",
GREY,
BOLD,
BOLD_RESET,
reason.trim(),
COLOR_RESET,
),
};
}
let _ = writeln!(buf);
}
// Summary at the end.
let _ = writeln!(
buf,
"{} cases: {}{}{} cases succeeded, {}{}{} cases failed in {} seconds",
number_of_successes + number_of_failures,
GREEN,
number_of_successes,
COLOR_RESET,
RED,
number_of_failures,
COLOR_RESET,
start.elapsed().as_secs()
);
}
#[allow(clippy::too_many_arguments)]
#[instrument(
level = "info",
name = "Handling Case"
skip_all,
fields(
metadata_file_path = %test.metadata.relative_path().display(),
mode = %test.mode,
case_idx = %test.case_idx,
case_name = test.case.name.as_deref().unwrap_or("Unnamed Case"),
)
)]
async fn handle_case_driver<'a>(
test: &Test<'a>,
cached_compiler: Arc<CachedCompiler<'a>>,
) -> anyhow::Result<usize> {
let platform_state = stream::iter(test.platforms.iter())
// Compiling the pre-link contracts.
.filter_map(|(platform, node, compiler, reporter)| {
let cached_compiler = cached_compiler.clone();
async move {
let compiler_output = cached_compiler
.compile_contracts(
test.metadata,
test.metadata_file_path,
test.mode.clone(),
None,
compiler.as_ref(),
*platform,
reporter,
)
.await
.inspect_err(|err| {
error!(
?err,
platform_identifier = %platform.platform_identifier(),
"Pre-linking compilation failed"
)
})
.ok()?;
Some((test, platform, node, compiler, reporter, compiler_output))
}
})
// Deploying the libraries for the platform.
.filter_map(
|(test, platform, node, compiler, reporter, compiler_output)| async move {
let mut deployed_libraries = None::<HashMap<_, _>>;
let mut contract_sources = test
.metadata
.contract_sources()
.inspect_err(|err| {
error!(
?err,
platform_identifier = %platform.platform_identifier(),
"Failed to retrieve contract sources from metadata"
)
})
.ok()?;
for library_instance in test
.metadata
.libraries
.iter()
.flatten()
.flat_map(|(_, map)| map.values())
{
debug!(%library_instance, "Deploying Library Instance");
let ContractPathAndIdent {
contract_source_path: library_source_path,
contract_ident: library_ident,
} = contract_sources.remove(library_instance)?;
let (code, abi) = compiler_output
.contracts
.get(&library_source_path)
.and_then(|contracts| contracts.get(library_ident.as_str()))?;
let code = alloy::hex::decode(code).ok()?;
// Getting the deployer address from the cases themselves. This is to ensure
// that we're doing the deployments from different accounts and therefore we're
// not slowed down by the nonce.
let deployer_address = test
.case
.steps
.iter()
.filter_map(|step| match step {
Step::FunctionCall(input) => Some(input.caller),
Step::BalanceAssertion(..) => None,
Step::StorageEmptyAssertion(..) => None,
Step::Repeat(..) => None,
})
.next()
.unwrap_or(FunctionCallStep::default_caller());
let tx = TransactionBuilder::<Ethereum>::with_deploy_code(
TransactionRequest::default().from(deployer_address),
code,
);
let receipt = node
.execute_transaction(tx)
.await
.inspect_err(|err| {
error!(
?err,
%library_instance,
platform_identifier = %platform.platform_identifier(),
"Failed to deploy the library"
)
})
.ok()?;
debug!(
?library_instance,
platform_identifier = %platform.platform_identifier(),
"Deployed library"
);
let library_address = receipt.contract_address?;
deployed_libraries.get_or_insert_default().insert(
library_instance.clone(),
(library_ident.clone(), library_address, abi.clone()),
);
}
Some((
test,
platform,
node,
compiler,
reporter,
compiler_output,
deployed_libraries,
))
},
)
// Compiling the post-link contracts.
.filter_map(
|(test, platform, node, compiler, reporter, _, deployed_libraries)| {
let cached_compiler = cached_compiler.clone();
async move {
let compiler_output = cached_compiler
.compile_contracts(
test.metadata,
test.metadata_file_path,
test.mode.clone(),
deployed_libraries.as_ref(),
compiler.as_ref(),
*platform,
reporter,
)
.await
.inspect_err(|err| {
error!(
?err,
platform_identifier = %platform.platform_identifier(),
"Pre-linking compilation failed"
)
})
.ok()?;
let case_state = CaseState::new(
compiler.version().clone(),
compiler_output.contracts,
deployed_libraries.unwrap_or_default(),
reporter.clone(),
);
Some((*node, platform.platform_identifier(), case_state))
}
},
)
// Collect
.collect::<Vec<_>>()
.await;
let mut driver = CaseDriver::new(test.metadata, test.case, platform_state);
driver
.execute()
.await
.inspect(|steps_executed| info!(steps_executed, "Case succeeded"))
}
async fn execute_corpus(
context: TestExecutionContext,
tests: &[MetadataFile],
reporter: Reporter,
report_aggregator_task: impl Future<Output = anyhow::Result<()>>,
) -> anyhow::Result<()> {
let platforms = context
.platforms
.iter()
.copied()
.collect::<BTreeSet<_>>()
.into_iter()
.map(Into::<&dyn Platform>::into)
.collect::<Vec<_>>();
run_driver(context, tests, reporter, report_aggregator_task, platforms).await?;
Ok(())
}
/// this represents a single "test"; a mode, path and collection of cases.
#[allow(clippy::type_complexity)]
struct Test<'a> {
metadata: &'a MetadataFile,
metadata_file_path: &'a Path,
mode: Cow<'a, Mode>,
case_idx: CaseIdx,
case: &'a Case,
platforms: Vec<(
&'a dyn Platform,
&'a dyn EthereumNode,
Box<dyn SolidityCompiler>,
ExecutionSpecificReporter,
)>,
reporter: TestSpecificReporter,
}
impl<'a> Test<'a> {
/// Checks if this test can be ran with the current configuration.
pub fn check_compatibility(&self) -> TestCheckFunctionResult {
self.check_metadata_file_ignored()?;
self.check_case_file_ignored()?;
self.check_target_compatibility()?;
self.check_evm_version_compatibility()?;
self.check_compiler_compatibility()?;
Ok(())
}
/// Checks if the metadata file is ignored or not.
fn check_metadata_file_ignored(&self) -> TestCheckFunctionResult {
if self.metadata.ignore.is_some_and(|ignore| ignore) {
Err(("Metadata file is ignored.", indexmap! {}))
} else {
Ok(())
}
}
/// Checks if the case file is ignored or not.
fn check_case_file_ignored(&self) -> TestCheckFunctionResult {
if self.case.ignore.is_some_and(|ignore| ignore) {
Err(("Case is ignored.", indexmap! {}))
} else {
Ok(())
}
}
/// Checks if the platforms all support the desired targets in the metadata file.
fn check_target_compatibility(&self) -> TestCheckFunctionResult {
let mut error_map = indexmap! {
"test_desired_targets" => json!(self.metadata.targets.as_ref()),
};
let mut is_allowed = true;
for (platform, ..) in self.platforms.iter() {
let is_allowed_for_platform = match self.metadata.targets.as_ref() {
None => true,
Some(targets) => {
let mut target_matches = false;
for target in targets.iter() {
if &platform.vm_identifier() == target {
target_matches = true;
break;
}
}
target_matches
}
};
is_allowed &= is_allowed_for_platform;
error_map.insert(
platform.platform_identifier().into(),
json!(is_allowed_for_platform),
);
}
if is_allowed {
Ok(())
} else {
Err((
"One of the platforms do do not support the targets allowed by the test.",
error_map,
))
}
}
// Checks for the compatibility of the EVM version with the platforms specified.
fn check_evm_version_compatibility(&self) -> TestCheckFunctionResult {
let Some(evm_version_requirement) = self.metadata.required_evm_version else {
return Ok(());
};
let mut error_map = indexmap! {
"test_desired_evm_version" => json!(self.metadata.required_evm_version),
};
let mut is_allowed = true;
for (platform, node, ..) in self.platforms.iter() {
let is_allowed_for_platform = evm_version_requirement.matches(&node.evm_version());
is_allowed &= is_allowed_for_platform;
error_map.insert(
platform.platform_identifier().into(),
json!(is_allowed_for_platform),
);
}
if is_allowed {
Ok(())
} else {
Err((
"EVM version is incompatible for the platforms specified",
error_map,
))
}
}
/// Checks if the platforms compilers support the mode that the test is for.
fn check_compiler_compatibility(&self) -> TestCheckFunctionResult {
let mut error_map = indexmap! {
"test_desired_evm_version" => json!(self.metadata.required_evm_version),
};
let mut is_allowed = true;
for (platform, _, compiler, ..) in self.platforms.iter() {
let is_allowed_for_platform =
compiler.supports_mode(self.mode.optimize_setting, self.mode.pipeline);
is_allowed &= is_allowed_for_platform;
error_map.insert(
platform.platform_identifier().into(),
json!(is_allowed_for_platform),
);
}
if is_allowed {
Ok(())
} else {
Err((
"Compilers do not support this mode either for the provided platforms.",
error_map,
))
}
}
}
type TestCheckFunctionResult = Result<(), (&'static str, IndexMap<&'static str, Value>)>;
-2
View File
@@ -14,8 +14,6 @@ revive-dt-common = { workspace = true }
revive-common = { workspace = true }
alloy = { workspace = true }
alloy-primitives = { workspace = true }
alloy-sol-types = { workspace = true }
anyhow = { workspace = true }
futures = { workspace = true }
regex = { workspace = true }
+19 -5
View File
@@ -3,10 +3,7 @@ use serde::{Deserialize, Serialize};
use revive_dt_common::{macros::define_wrapper_type, types::Mode};
use crate::{
input::{Expected, Step},
mode::ParsedMode,
};
use crate::{mode::ParsedMode, steps::*};
#[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq, JsonSchema)]
pub struct Case {
@@ -55,7 +52,6 @@ pub struct Case {
}
impl Case {
#[allow(irrefutable_let_patterns)]
pub fn steps_iterator(&self) -> impl Iterator<Item = Step> {
let steps_len = self.steps.len();
self.steps
@@ -84,6 +80,24 @@ impl Case {
})
}
pub fn steps_iterator_for_benchmarks(
&self,
default_repeat_count: usize,
) -> Box<dyn Iterator<Item = Step> + '_> {
let contains_repeat = self
.steps_iterator()
.any(|step| matches!(&step, Step::Repeat(..)));
if contains_repeat {
Box::new(self.steps_iterator()) as Box<_>
} else {
Box::new(std::iter::once(Step::Repeat(Box::new(RepeatStep {
comment: None,
repeat: default_repeat_count,
steps: self.steps_iterator().collect(),
})))) as Box<_>
}
}
pub fn solc_modes(&self) -> Vec<Mode> {
match &self.modes {
Some(modes) => ParsedMode::many_to_modes(modes.iter()).collect(),
+1 -1
View File
@@ -2,7 +2,7 @@
pub mod case;
pub mod corpus;
pub mod input;
pub mod metadata;
pub mod mode;
pub mod steps;
pub mod traits;
@@ -1,5 +1,6 @@
use std::collections::HashMap;
use std::{collections::HashMap, fmt::Display, str::FromStr};
use alloy::primitives::{FixedBytes, utils::parse_units};
use alloy::{
eips::BlockNumberOrTag,
json_abi::Function,
@@ -7,7 +8,6 @@ use alloy::{
primitives::{Address, Bytes, U256},
rpc::types::TransactionRequest,
};
use alloy_primitives::{FixedBytes, utils::parse_units};
use anyhow::Context as _;
use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, stream};
use schemars::JsonSchema;
@@ -29,19 +29,88 @@ use crate::{metadata::ContractInstance, traits::ResolutionContext};
pub enum Step {
/// A function call or an invocation to some function on some smart contract.
FunctionCall(Box<FunctionCallStep>),
/// A step for performing a balance assertion on some account or contract.
BalanceAssertion(Box<BalanceAssertionStep>),
/// A step for asserting that the storage of some contract or account is empty.
StorageEmptyAssertion(Box<StorageEmptyAssertionStep>),
/// A special step for repeating a bunch of steps a certain number of times.
Repeat(Box<RepeatStep>),
/// A step type that allows for a new account address to be allocated and to later on be used
/// as the caller in another step.
AllocateAccount(Box<AllocateAccountStep>),
}
define_wrapper_type!(
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct StepIdx(usize) impl Display;
pub struct StepIdx(usize) impl Display, FromStr;
);
define_wrapper_type!(
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct StepPath(Vec<StepIdx>);
);
impl StepPath {
pub fn from_iterator(path: impl IntoIterator<Item = impl Into<StepIdx>>) -> Self {
Self(path.into_iter().map(|value| value.into()).collect())
}
pub fn increment(&self) -> Self {
let mut this = self.clone();
if let Some(last) = this.last_mut() {
last.0 += 1
}
this
}
pub fn append(&self, step_idx: impl Into<StepIdx>) -> Self {
let mut this = self.clone();
this.0.push(step_idx.into());
this
}
}
impl Display for StepPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0
.iter()
.map(|idx| idx.to_string())
.collect::<Vec<_>>()
.join(".")
.fmt(f)
}
}
impl FromStr for StepPath {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.split(".")
.map(StepIdx::from_str)
.collect::<anyhow::Result<Vec<_>>>()
.map(Self)
}
}
impl From<StepPath> for String {
fn from(value: StepPath) -> Self {
value.to_string()
}
}
impl TryFrom<String> for StepPath {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
value.parse()
}
}
/// This is an input step which is a transaction description that the framework translates into a
/// transaction and executes on the nodes.
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
@@ -49,7 +118,7 @@ pub struct FunctionCallStep {
/// The address of the account performing the call and paying the fees for it.
#[serde(default = "FunctionCallStep::default_caller")]
#[schemars(with = "String")]
pub caller: Address,
pub caller: StepAddress,
/// An optional comment on the step which has no impact on the execution in any way.
#[serde(skip_serializing_if = "Option::is_none")]
@@ -86,7 +155,7 @@ pub struct FunctionCallStep {
/// This represents a balance assertion step where the framework needs to query the balance of some
/// account or contract and assert that it's some amount.
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
pub struct BalanceAssertionStep {
/// An optional comment on the balance assertion.
#[serde(skip_serializing_if = "Option::is_none")]
@@ -98,7 +167,7 @@ pub struct BalanceAssertionStep {
/// this could be a normal hex address, a variable such as `Test.address`, or perhaps even a
/// full on variable like `$VARIABLE:Uniswap`. It follows the same resolution rules that are
/// followed in the calldata.
pub address: String,
pub address: StepAddress,
/// The amount of balance to assert that the account or contract has. This is a 256 bit string
/// that's serialized and deserialized into a decimal string.
@@ -108,7 +177,7 @@ pub struct BalanceAssertionStep {
/// This represents an assertion for the storage of some contract or account and whether it's empty
/// or not.
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
pub struct StorageEmptyAssertionStep {
/// An optional comment on the storage empty assertion.
#[serde(skip_serializing_if = "Option::is_none")]
@@ -120,7 +189,7 @@ pub struct StorageEmptyAssertionStep {
/// this could be a normal hex address, a variable such as `Test.address`, or perhaps even a
/// full on variable like `$VARIABLE:Uniswap`. It follows the same resolution rules that are
/// followed in the calldata.
pub address: String,
pub address: StepAddress,
/// A boolean of whether the storage of the address is empty or not.
pub is_storage_empty: bool,
@@ -130,6 +199,10 @@ pub struct StorageEmptyAssertionStep {
/// steps to be repeated (on different drivers) a certain number of times.
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
pub struct RepeatStep {
/// An optional comment on the repetition step.
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
/// The number of repetitions that the steps should be repeated for.
pub repeat: usize,
@@ -137,6 +210,19 @@ pub struct RepeatStep {
pub steps: Vec<Step>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
pub struct AllocateAccountStep {
/// An optional comment on the account allocation step.
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
/// An instruction to allocate a new account with the value being the variable name of that
/// account. This must start with `$VARIABLE:` and then be followed by the variable name of the
/// account.
#[serde(rename = "allocate_account")]
pub variable_name: String,
}
/// A set of expectations and assertions to make about the transaction after it ran.
///
/// If this is not specified then the only assertion that will be ran is that the transaction
@@ -177,7 +263,7 @@ pub struct ExpectedOutput {
pub struct Event {
/// An optional field of the address of the emitter of the event.
#[serde(skip_serializing_if = "Option::is_none")]
pub address: Option<String>,
pub address: Option<StepAddress>,
/// The set of topics to expect the event to have.
pub topics: Vec<String>,
@@ -310,13 +396,74 @@ pub struct VariableAssignments {
pub return_data: Vec<String>,
}
/// An address type that might either be an address literal or a resolvable address.
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
#[schemars(with = "String")]
#[serde(untagged)]
pub enum StepAddress {
Address(Address),
ResolvableAddress(String),
}
impl Default for StepAddress {
fn default() -> Self {
Self::Address(Default::default())
}
}
impl Display for StepAddress {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
StepAddress::Address(address) => Display::fmt(address, f),
StepAddress::ResolvableAddress(address) => Display::fmt(address, f),
}
}
}
impl StepAddress {
pub fn as_address(&self) -> Option<&Address> {
match self {
StepAddress::Address(address) => Some(address),
StepAddress::ResolvableAddress(_) => None,
}
}
pub fn as_resolvable_address(&self) -> Option<&str> {
match self {
StepAddress::ResolvableAddress(address) => Some(address),
StepAddress::Address(..) => None,
}
}
pub async fn resolve_address(
&self,
resolver: &(impl ResolverApi + ?Sized),
context: ResolutionContext<'_>,
) -> anyhow::Result<Address> {
match self {
StepAddress::Address(address) => Ok(*address),
StepAddress::ResolvableAddress(address) => Ok(Address::from_slice(
Calldata::new_compound([address])
.calldata(resolver, context)
.await?
.get(12..32)
.expect("Can't fail"),
)),
}
}
}
impl FunctionCallStep {
pub const fn default_caller() -> Address {
pub const fn default_caller_address() -> Address {
Address(FixedBytes(alloy::hex!(
"0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"
)))
}
pub const fn default_caller() -> StepAddress {
StepAddress::Address(Self::default_caller_address())
}
fn default_instance() -> ContractInstance {
ContractInstance::new("Test")
}
@@ -390,7 +537,7 @@ impl FunctionCallStep {
}
/// Parse this input into a legacy transaction.
pub async fn legacy_transaction(
pub async fn as_transaction(
&self,
resolver: &(impl ResolverApi + ?Sized),
context: ResolutionContext<'_>,
@@ -399,7 +546,8 @@ impl FunctionCallStep {
.encoded_input(resolver, context)
.await
.context("Failed to encode input bytes for transaction request")?;
let transaction_request = TransactionRequest::default().from(self.caller).value(
let caller = self.caller.resolve_address(resolver, context).await?;
let transaction_request = TransactionRequest::default().from(caller).value(
self.value
.map(|value| value.into_inner())
.unwrap_or_default(),
@@ -811,9 +959,9 @@ impl<'de> Deserialize<'de> for EtherValue {
#[cfg(test)]
mod tests {
use alloy::primitives::{BlockHash, BlockNumber, BlockTimestamp, ChainId, TxHash, address};
use alloy::sol_types::SolValue;
use alloy::{eips::BlockNumberOrTag, json_abi::JsonAbi};
use alloy_primitives::{BlockHash, BlockNumber, BlockTimestamp, ChainId, TxHash, address};
use alloy_sol_types::SolValue;
use std::{collections::HashMap, pin::Pin};
use super::*;
@@ -967,7 +1115,7 @@ mod tests {
let encoded = input.encoded_input(&resolver, context).await.unwrap();
assert!(encoded.0.starts_with(&selector));
type T = (alloy_primitives::Address,);
type T = (alloy::primitives::Address,);
let decoded: T = T::abi_decode(&encoded.0[4..]).unwrap();
assert_eq!(
decoded.0,
@@ -1014,7 +1162,7 @@ mod tests {
let encoded = input.encoded_input(&resolver, context).await.unwrap();
assert!(encoded.0.starts_with(&selector));
type T = (alloy_primitives::Address,);
type T = (alloy::primitives::Address,);
let decoded: T = T::abi_decode(&encoded.0[4..]).unwrap();
assert_eq!(
decoded.0,
+1 -1
View File
@@ -3,8 +3,8 @@ use std::pin::Pin;
use alloy::eips::BlockNumberOrTag;
use alloy::json_abi::JsonAbi;
use alloy::primitives::TxHash;
use alloy::primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, ChainId, U256};
use alloy_primitives::TxHash;
use anyhow::Result;
use crate::metadata::{ContractIdent, ContractInstance};
+1
View File
@@ -15,6 +15,7 @@ revive-dt-format = { workspace = true }
alloy = { workspace = true }
anyhow = { workspace = true }
futures = { workspace = true }
[lints]
workspace = true
+43 -1
View File
@@ -3,22 +3,36 @@
use std::pin::Pin;
use std::sync::Arc;
use alloy::primitives::{Address, StorageKey, TxHash, U256};
use alloy::primitives::{Address, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256};
use alloy::rpc::types::trace::geth::{DiffMode, GethDebugTracingOptions, GethTrace};
use alloy::rpc::types::{EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest};
use anyhow::Result;
use futures::Stream;
use revive_common::EVMVersion;
use revive_dt_format::traits::ResolverApi;
/// An interface for all interactions with Ethereum compatible nodes.
#[allow(clippy::type_complexity)]
pub trait EthereumNode {
/// A function to run post spawning the nodes and before any transactions are run on the node.
fn pre_transactions(&mut self) -> Pin<Box<dyn Future<Output = anyhow::Result<()>> + '_>>;
fn id(&self) -> usize;
/// Returns the nodes connection string.
fn connection_string(&self) -> &str;
fn submit_transaction(
&self,
transaction: TransactionRequest,
) -> Pin<Box<dyn Future<Output = Result<TxHash>> + '_>>;
fn get_receipt(
&self,
tx_hash: TxHash,
) -> Pin<Box<dyn Future<Output = Result<TransactionReceipt>> + '_>>;
/// Execute the [TransactionRequest] and return a [TransactionReceipt].
fn execute_transaction(
&self,
@@ -50,4 +64,32 @@ pub trait EthereumNode {
/// Returns the EVM version of the node.
fn evm_version(&self) -> EVMVersion;
/// Returns a stream of the blocks that were mined by the node.
fn subscribe_to_full_blocks_information(
&self,
) -> Pin<
Box<
dyn Future<Output = anyhow::Result<Pin<Box<dyn Stream<Item = MinedBlockInformation>>>>>
+ '_,
>,
>;
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct MinedBlockInformation {
/// The block number.
pub block_number: BlockNumber,
/// The block timestamp.
pub block_timestamp: BlockTimestamp,
/// The amount of gas mined in the block.
pub mined_gas: u128,
/// The gas limit of the block.
pub block_gas_limit: u128,
/// The hashes of the transactions that were mined as part of the block.
pub transaction_hashes: Vec<TxHash>,
}
+6
View File
@@ -11,7 +11,10 @@ rust-version.workspace = true
[dependencies]
anyhow = { workspace = true }
alloy = { workspace = true }
async-stream = { workspace = true }
futures = { workspace = true }
tracing = { workspace = true }
tower = { workspace = true }
tokio = { workspace = true }
revive-common = { workspace = true }
@@ -22,9 +25,12 @@ revive-dt-node-interaction = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_with = { workspace = true }
serde_yaml_ng = { workspace = true }
sp-core = { workspace = true }
sp-runtime = { workspace = true }
zombienet-sdk = { workspace = true }
[dev-dependencies]
temp-dir = { workspace = true }
+5
View File
@@ -1,5 +1,10 @@
use alloy::primitives::ChainId;
/// This constant defines how much Wei accounts are pre-seeded with in genesis.
///
/// Note: After changing this number, check that the tests for substrate work as we encountered
/// some issues with different values of the initial balance on substrate.
pub const INITIAL_BALANCE: u128 = 10u128.pow(37);
/// The chain id used for all of the chains spawned by the framework.
pub const CHAIN_ID: ChainId = 420420420;
+3
View File
@@ -0,0 +1,3 @@
mod process;
pub use process::*;
+191
View File
@@ -0,0 +1,191 @@
use std::{
fs::{File, OpenOptions},
io::{BufRead, BufReader, Write},
path::Path,
process::{Child, Command},
time::{Duration, Instant},
};
use anyhow::{Context, Result, bail};
/// A wrapper around processes which allows for their stdout and stderr to be logged and flushed
/// when the process is dropped.
#[derive(Debug)]
pub struct Process {
/// The handle of the child process.
child: Child,
/// The file that stdout is being logged to.
stdout_logs_file: File,
/// The file that stderr is being logged to.
stderr_logs_file: File,
}
impl Process {
pub fn new(
log_file_prefix: impl Into<Option<&'static str>>,
logs_directory: impl AsRef<Path>,
binary_path: impl AsRef<Path>,
command_building_callback: impl FnOnce(&mut Command, File, File),
process_readiness_wait_behavior: ProcessReadinessWaitBehavior,
) -> Result<Self> {
let log_file_prefix = log_file_prefix.into();
let (stdout_file_name, stderr_file_name) = match log_file_prefix {
Some(prefix) => (
format!("{prefix}_stdout.log"),
format!("{prefix}_stderr.log"),
),
None => ("stdout.log".to_string(), "stderr.log".to_string()),
};
let stdout_logs_file_path = logs_directory.as_ref().join(stdout_file_name);
let stderr_logs_file_path = logs_directory.as_ref().join(stderr_file_name);
let stdout_logs_file = OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(stdout_logs_file_path.as_path())
.context("Failed to open the stdout logs file")?;
let stderr_logs_file = OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(stderr_logs_file_path.as_path())
.context("Failed to open the stderr logs file")?;
let mut command = {
let stdout_logs_file = stdout_logs_file
.try_clone()
.context("Failed to clone the stdout logs file")?;
let stderr_logs_file = stderr_logs_file
.try_clone()
.context("Failed to clone the stderr logs file")?;
let mut command = Command::new(binary_path.as_ref());
command_building_callback(&mut command, stdout_logs_file, stderr_logs_file);
command
};
let mut child = command
.spawn()
.context("Failed to spawn the built command")?;
match process_readiness_wait_behavior {
ProcessReadinessWaitBehavior::NoStartupWait => {}
ProcessReadinessWaitBehavior::WaitDuration(duration) => std::thread::sleep(duration),
ProcessReadinessWaitBehavior::TimeBoundedWaitFunction {
max_wait_duration,
mut check_function,
} => {
let spawn_time = Instant::now();
let stdout_logs_file = OpenOptions::new()
.read(true)
.open(stdout_logs_file_path)
.context("Failed to open the stdout logs file")?;
let stderr_logs_file = OpenOptions::new()
.read(true)
.open(stderr_logs_file_path)
.context("Failed to open the stderr logs file")?;
let mut stdout_lines = BufReader::new(stdout_logs_file).lines();
let mut stderr_lines = BufReader::new(stderr_logs_file).lines();
let mut stdout = String::new();
let mut stderr = String::new();
loop {
let stdout_line = stdout_lines.next().and_then(Result::ok);
let stderr_line = stderr_lines.next().and_then(Result::ok);
if let Some(stdout_line) = stdout_line.as_ref() {
stdout.push_str(stdout_line);
stdout.push('\n');
}
if let Some(stderr_line) = stderr_line.as_ref() {
stderr.push_str(stderr_line);
stderr.push('\n');
}
let check_result =
check_function(stdout_line.as_deref(), stderr_line.as_deref()).context(
format!(
"Failed to wait for the process to be ready - {stdout} - {stderr}"
),
)?;
if check_result {
break;
}
if Instant::now().duration_since(spawn_time) > max_wait_duration {
bail!(
"Waited for the process to start but it failed to start in time. stderr {stderr} - stdout {stdout}"
)
}
}
}
ProcessReadinessWaitBehavior::WaitForCommandToExit => {
if !child
.wait()
.context("Failed waiting for process to finish")?
.success()
{
anyhow::bail!("Failed to spawn command");
}
}
}
Ok(Self {
child,
stdout_logs_file,
stderr_logs_file,
})
}
}
impl Drop for Process {
fn drop(&mut self) {
self.child.kill().expect("Failed to kill the process");
self.stdout_logs_file
.flush()
.expect("Failed to flush the stdout logs file");
self.stderr_logs_file
.flush()
.expect("Failed to flush the stderr logs file");
}
}
pub enum ProcessReadinessWaitBehavior {
/// The process does not require any kind of wait after it's been spawned and can be used
/// straight away.
NoStartupWait,
/// Waits for the command to exit.
WaitForCommandToExit,
/// The process does require some amount of wait duration after it's been started.
WaitDuration(Duration),
/// The process requires a time bounded wait function which is a function of the lines that
/// appear in the log files.
TimeBoundedWaitFunction {
/// The maximum amount of time to wait for the check function to return true.
max_wait_duration: Duration,
/// The function to use to check if the process spawned is ready to use or not. This
/// function should return the following in the following cases:
///
/// - `Ok(true)`: Returned when the condition the process is waiting for has been fulfilled
/// and the wait is completed.
/// - `Ok(false)`: The process is not ready yet but it might be ready in the future.
/// - `Err`: The process is not ready yet and will not be ready in the future as it appears
/// that it has encountered an error when it was being spawned.
///
/// The first argument is a line from stdout and the second argument is a line from stderr.
#[allow(clippy::type_complexity)]
check_function: Box<dyn FnMut(Option<&str>, Option<&str>) -> anyhow::Result<bool>>,
},
}
+3 -3
View File
@@ -3,10 +3,10 @@
use alloy::genesis::Genesis;
use revive_dt_node_interaction::EthereumNode;
pub mod common;
pub mod constants;
pub mod geth;
pub mod substrate;
pub mod helpers;
pub mod node_implementations;
pub mod provider_utils;
/// An abstract interface for testing nodes.
pub trait Node: EthereumNode {
@@ -1,17 +1,17 @@
//! The go-ethereum node implementation.
use std::{
fs::{File, OpenOptions, create_dir_all, remove_dir_all},
io::{BufRead, BufReader, Read, Write},
fs::{File, create_dir_all, remove_dir_all},
io::Read,
ops::ControlFlow,
path::PathBuf,
pin::Pin,
process::{Child, Command, Stdio},
process::{Command, Stdio},
sync::{
Arc,
atomic::{AtomicU32, Ordering},
},
time::{Duration, Instant},
time::Duration,
};
use alloy::{
@@ -20,18 +20,22 @@ use alloy::{
network::{Ethereum, EthereumWallet, NetworkWallet},
primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256},
providers::{
Provider, ProviderBuilder,
Provider,
ext::DebugApi,
fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller},
fillers::{CachedNonceManager, ChainIdFiller, NonceFiller},
},
rpc::types::{
EIP1186AccountProofResponse, TransactionRequest,
trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame},
EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest,
trace::geth::{
DiffMode, GethDebugTracingOptions, GethTrace, PreStateConfig, PreStateFrame,
},
},
};
use anyhow::Context as _;
use futures::{Stream, StreamExt};
use revive_common::EVMVersion;
use tracing::{Instrument, instrument};
use tokio::sync::OnceCell;
use tracing::{Instrument, error, instrument};
use revive_dt_common::{
fs::clear_directory,
@@ -39,9 +43,14 @@ use revive_dt_common::{
};
use revive_dt_config::*;
use revive_dt_format::traits::ResolverApi;
use revive_dt_node_interaction::EthereumNode;
use revive_dt_node_interaction::{EthereumNode, MinedBlockInformation};
use crate::{Node, common::FallbackGasFiller, constants::INITIAL_BALANCE};
use crate::{
Node,
constants::{CHAIN_ID, INITIAL_BALANCE},
helpers::{Process, ProcessReadinessWaitBehavior},
provider_utils::{ConcreteProvider, FallbackGasFiller, construct_concurrency_limited_provider},
};
static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
@@ -61,16 +70,11 @@ pub struct GethNode {
logs_directory: PathBuf,
geth: PathBuf,
id: u32,
handle: Option<Child>,
handle: Option<Process>,
start_timeout: Duration,
wallet: Arc<EthereumWallet>,
nonce_manager: CachedNonceManager,
chain_id_filler: ChainIdFiller,
/// 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
/// separate fields) as the logic that we need to apply to them is all the same regardless of
/// what it belongs to, we just want to flush them on [`Drop`] of the node.
logs_file_to_flush: Vec<File>,
provider: OnceCell<ConcreteProvider<Ethereum, Arc<EthereumWallet>>>,
}
impl GethNode {
@@ -84,9 +88,6 @@ impl GethNode {
const READY_MARKER: &str = "IPC endpoint opened";
const ERROR_MARKER: &str = "Fatal:";
const GETH_STDOUT_LOG_FILE_NAME: &str = "node_stdout.log";
const GETH_STDERR_LOG_FILE_NAME: &str = "node_stderr.log";
const TRANSACTION_INDEXING_ERROR: &str = "transaction indexing is in progress";
const TRANSACTION_TRACING_ERROR: &str = "historical state not available in path scheme yet";
@@ -122,11 +123,8 @@ impl GethNode {
handle: None,
start_timeout: geth_configuration.start_timeout_ms,
wallet: wallet.clone(),
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
// the vector. It's the stdout and stderr of the geth node.
logs_file_to_flush: Vec::with_capacity(2),
provider: Default::default(),
}
}
@@ -194,139 +192,86 @@ impl GethNode {
/// [Instance::init] must be called prior.
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
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
// opening in this method. We need to construct it in this way to:
// 1. Be consistent
// 2. Less verbose and more dry
// 3. Because the builder pattern uses mutable references so we need to get around that.
let open_options = {
let mut options = OpenOptions::new();
options.create(true).truncate(true).write(true);
options
};
let process = Process::new(
None,
self.logs_directory.as_path(),
self.geth.as_path(),
|command, stdout_file, stderr_file| {
command
.arg("--dev")
.arg("--datadir")
.arg(&self.data_directory)
.arg("--ipcpath")
.arg(&self.connection_string)
.arg("--nodiscover")
.arg("--maxpeers")
.arg("0")
.arg("--txlookuplimit")
.arg("0")
.arg("--cache.blocklogs")
.arg("512")
.arg("--state.scheme")
.arg("hash")
.arg("--syncmode")
.arg("full")
.arg("--gcmode")
.arg("archive")
.stderr(stderr_file)
.stdout(stdout_file);
},
ProcessReadinessWaitBehavior::TimeBoundedWaitFunction {
max_wait_duration: self.start_timeout,
check_function: Box::new(|_, stderr_line| match stderr_line {
Some(line) => {
if line.contains(Self::ERROR_MARKER) {
anyhow::bail!("Failed to start geth {line}");
} else if line.contains(Self::READY_MARKER) {
Ok(true)
} else {
Ok(false)
}
}
None => Ok(false),
}),
},
);
let stdout_logs_file = open_options
.clone()
.open(self.geth_stdout_log_file_path())
.context("Failed to open geth stdout logs file")?;
let stderr_logs_file = open_options
.open(self.geth_stderr_log_file_path())
.context("Failed to open geth stderr logs file")?;
self.handle = Command::new(&self.geth)
.arg("--dev")
.arg("--datadir")
.arg(&self.data_directory)
.arg("--ipcpath")
.arg(&self.connection_string)
.arg("--nodiscover")
.arg("--maxpeers")
.arg("0")
.arg("--txlookuplimit")
.arg("0")
.arg("--cache.blocklogs")
.arg("512")
.arg("--state.scheme")
.arg("hash")
.arg("--syncmode")
.arg("full")
.arg("--gcmode")
.arg("archive")
.stderr(
stderr_logs_file
.try_clone()
.context("Failed to clone geth stderr log file handle")?,
)
.stdout(
stdout_logs_file
.try_clone()
.context("Failed to clone geth stdout log file handle")?,
)
.spawn()
.context("Failed to spawn geth node process")?
.into();
if let Err(error) = self.wait_ready() {
tracing::error!(?error, "Failed to start geth, shutting down gracefully");
self.shutdown()
.context("Failed to gracefully shutdown after geth start error")?;
return Err(error);
match process {
Ok(process) => self.handle = Some(process),
Err(err) => {
error!(?err, "Failed to start geth, shutting down gracefully");
self.shutdown()
.context("Failed to gracefully shutdown after geth start error")?;
return Err(err);
}
}
self.logs_file_to_flush
.extend([stderr_logs_file, stdout_logs_file]);
Ok(self)
}
/// Wait for the g-ethereum node child process getting ready.
///
/// [Instance::spawn_process] must be called priorly.
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
fn wait_ready(&mut self) -> anyhow::Result<&mut Self> {
let start_time = Instant::now();
let logs_file = OpenOptions::new()
.read(true)
.write(false)
.append(false)
.truncate(false)
.open(self.geth_stderr_log_file_path())
.context("Failed to open geth stderr logs file for readiness check")?;
let maximum_wait_time = self.start_timeout;
let mut stderr = BufReader::new(logs_file).lines();
let mut lines = vec![];
loop {
if let Some(Ok(line)) = stderr.next() {
if line.contains(Self::ERROR_MARKER) {
anyhow::bail!("Failed to start geth {line}");
}
if line.contains(Self::READY_MARKER) {
return Ok(self);
}
lines.push(line);
}
if Instant::now().duration_since(start_time) > maximum_wait_time {
anyhow::bail!(
"Timeout in starting geth: took longer than {}ms. stdout:\n\n{}\n",
self.start_timeout.as_millis(),
lines.join("\n")
);
}
}
}
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
fn geth_stdout_log_file_path(&self) -> PathBuf {
self.logs_directory.join(Self::GETH_STDOUT_LOG_FILE_NAME)
}
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
fn geth_stderr_log_file_path(&self) -> PathBuf {
self.logs_directory.join(Self::GETH_STDERR_LOG_FILE_NAME)
}
async fn provider(
&self,
) -> anyhow::Result<FillProvider<impl TxFiller<Ethereum>, impl Provider<Ethereum>, Ethereum>>
{
ProviderBuilder::new()
.disable_recommended_fillers()
.filler(FallbackGasFiller::new(
25_000_000,
1_000_000_000,
1_000_000_000,
))
.filler(self.chain_id_filler.clone())
.filler(NonceFiller::new(self.nonce_manager.clone()))
.wallet(self.wallet.clone())
.connect(&self.connection_string)
async fn provider(&self) -> anyhow::Result<ConcreteProvider<Ethereum, Arc<EthereumWallet>>> {
self.provider
.get_or_try_init(|| async move {
construct_concurrency_limited_provider::<Ethereum, _>(
self.connection_string.as_str(),
FallbackGasFiller::default(),
ChainIdFiller::new(Some(CHAIN_ID)),
NonceFiller::new(self.nonce_manager.clone()),
self.wallet.clone(),
)
.await
.context("Failed to construct the provider")
})
.await
.map_err(Into::into)
.cloned()
}
}
impl EthereumNode for GethNode {
fn pre_transactions(&mut self) -> Pin<Box<dyn Future<Output = anyhow::Result<()>> + '_>> {
Box::pin(async move { Ok(()) })
}
fn id(&self) -> usize {
self.id as _
}
@@ -335,6 +280,50 @@ impl EthereumNode for GethNode {
&self.connection_string
}
#[instrument(
level = "info",
skip_all,
fields(geth_node_id = self.id, connection_string = self.connection_string),
err,
)]
fn submit_transaction(
&self,
transaction: TransactionRequest,
) -> Pin<Box<dyn Future<Output = anyhow::Result<TxHash>> + '_>> {
Box::pin(async move {
let provider = self
.provider()
.await
.context("Failed to create the provider for transaction submission")?;
let pending_transaction = provider
.send_transaction(transaction)
.await
.context("Failed to submit the transaction through the provider")?;
Ok(*pending_transaction.tx_hash())
})
}
#[instrument(
level = "info",
skip_all,
fields(geth_node_id = self.id, connection_string = self.connection_string),
err,
)]
fn get_receipt(
&self,
tx_hash: TxHash,
) -> Pin<Box<dyn Future<Output = anyhow::Result<TransactionReceipt>> + '_>> {
Box::pin(async move {
self.provider()
.await
.context("Failed to create provider for getting the receipt")?
.get_transaction_receipt(tx_hash)
.await
.context("Failed to get the receipt of the transaction")?
.context("Failed to get the receipt of the transaction")
})
}
#[instrument(
level = "info",
skip_all,
@@ -344,8 +333,7 @@ impl EthereumNode for GethNode {
fn execute_transaction(
&self,
transaction: TransactionRequest,
) -> Pin<Box<dyn Future<Output = anyhow::Result<alloy::rpc::types::TransactionReceipt>> + '_>>
{
) -> Pin<Box<dyn Future<Output = anyhow::Result<TransactionReceipt>> + '_>> {
Box::pin(async move {
let provider = self
.provider()
@@ -353,12 +341,12 @@ impl EthereumNode for GethNode {
.context("Failed to create provider for transaction submission")?;
let pending_transaction = provider
.send_transaction(transaction)
.await
.inspect_err(
|err| tracing::error!(%err, "Encountered an error when submitting the transaction"),
)
.context("Failed to submit transaction to geth node")?;
.send_transaction(transaction)
.await
.inspect_err(
|err| error!(%err, "Encountered an error when submitting the transaction"),
)
.context("Failed to submit transaction to geth node")?;
let transaction_hash = *pending_transaction.tx_hash();
// The following is a fix for the "transaction indexing is in progress" error that we used
@@ -378,7 +366,6 @@ impl EthereumNode for GethNode {
// allowed for 60 seconds of waiting with a 1 second delay in polling, we need to allow for
// a larger wait time. Therefore, in here we allow for 5 minutes of waiting with exponential
// backoff each time we attempt to get the receipt and find that it's not available.
let provider = Arc::new(provider);
poll(
Self::RECEIPT_POLLING_DURATION,
PollingWaitBehavior::Constant(Duration::from_millis(200)),
@@ -412,14 +399,12 @@ impl EthereumNode for GethNode {
&self,
tx_hash: TxHash,
trace_options: GethDebugTracingOptions,
) -> Pin<Box<dyn Future<Output = anyhow::Result<alloy::rpc::types::trace::geth::GethTrace>> + '_>>
{
) -> Pin<Box<dyn Future<Output = anyhow::Result<GethTrace>> + '_>> {
Box::pin(async move {
let provider = Arc::new(
self.provider()
.await
.context("Failed to create provider for tracing")?,
);
let provider = self
.provider()
.await
.context("Failed to create provider for tracing")?;
poll(
Self::TRACE_POLLING_DURATION,
PollingWaitBehavior::Constant(Duration::from_millis(200)),
@@ -517,14 +502,54 @@ impl EthereumNode for GethNode {
fn evm_version(&self) -> EVMVersion {
EVMVersion::Cancun
}
fn subscribe_to_full_blocks_information(
&self,
) -> Pin<
Box<
dyn Future<Output = anyhow::Result<Pin<Box<dyn Stream<Item = MinedBlockInformation>>>>>
+ '_,
>,
> {
Box::pin(async move {
let provider = self
.provider()
.await
.context("Failed to create the provider for block subscription")?;
let block_subscription = provider.subscribe_full_blocks();
let block_stream = block_subscription
.into_stream()
.await
.context("Failed to create the block stream")?;
let mined_block_information_stream = block_stream.filter_map(|block| async {
let block = block.ok()?;
Some(MinedBlockInformation {
block_number: block.number(),
block_timestamp: block.header.timestamp,
mined_gas: block.header.gas_used as _,
block_gas_limit: block.header.gas_limit as _,
transaction_hashes: block
.transactions
.into_hashes()
.as_hashes()
.expect("Must be hashes")
.to_vec(),
})
});
Ok(Box::pin(mined_block_information_stream)
as Pin<Box<dyn Stream<Item = MinedBlockInformation>>>)
})
}
}
pub struct GethNodeResolver<F: TxFiller<Ethereum>, P: Provider<Ethereum>> {
pub struct GethNodeResolver {
id: u32,
provider: FillProvider<F, P, Ethereum>,
provider: ConcreteProvider<Ethereum, Arc<EthereumWallet>>,
}
impl<F: TxFiller<Ethereum>, P: Provider<Ethereum>> ResolverApi for GethNodeResolver<F, P> {
impl ResolverApi for GethNodeResolver {
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
fn chain_id(
&self,
@@ -650,17 +675,7 @@ impl<F: TxFiller<Ethereum>, P: Provider<Ethereum>> ResolverApi for GethNodeResol
impl Node for GethNode {
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
fn shutdown(&mut self) -> anyhow::Result<()> {
// Terminate the processes in a graceful manner to allow for the output to be flushed.
if let Some(mut child) = self.handle.take() {
child
.kill()
.map_err(|error| anyhow::anyhow!("Failed to kill the geth process: {error:?}"))?;
}
// Flushing the files that we're using for keeping the logs before shutdown.
for file in self.logs_file_to_flush.iter_mut() {
file.flush()?
}
drop(self.handle.take());
// Remove the node's database so that subsequent runs do not run on the same database. We
// ignore the error just in case the directory didn't exist in the first place and therefore
@@ -701,6 +716,8 @@ impl Drop for GethNode {
#[cfg(test)]
mod tests {
use std::sync::LazyLock;
use super::*;
fn test_config() -> TestExecutionContext {
@@ -717,9 +734,47 @@ mod tests {
(context, node)
}
fn shared_state() -> &'static (TestExecutionContext, GethNode) {
static STATE: LazyLock<(TestExecutionContext, GethNode)> = LazyLock::new(new_node);
&STATE
}
fn shared_node() -> &'static GethNode {
&shared_state().1
}
#[tokio::test]
async fn node_mines_simple_transfer_transaction_and_returns_receipt() {
// Arrange
let (context, node) = shared_state();
let account_address = context
.wallet_configuration
.wallet()
.default_signer()
.address();
let transaction = TransactionRequest::default()
.to(account_address)
.value(U256::from(100_000_000_000_000u128));
// Act
let receipt = node.execute_transaction(transaction).await;
// Assert
let _ = receipt.expect("Failed to get the receipt for the transfer");
}
#[test]
#[ignore = "Ignored since they take a long time to run"]
fn version_works() {
let version = GethNode::new(&test_config()).version().unwrap();
// Arrange
let node = shared_node();
// Act
let version = node.version();
// Assert
let version = version.expect("Failed to get the version");
assert!(
version.starts_with("geth version"),
"expected version string, got: '{version}'"
@@ -727,9 +782,10 @@ mod tests {
}
#[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_chain_id_from_node() {
// Arrange
let (_context, node) = new_node();
let node = shared_node();
// Act
let chain_id = node.resolver().await.unwrap().chain_id().await;
@@ -740,9 +796,10 @@ mod tests {
}
#[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_gas_limit_from_node() {
// Arrange
let (_context, node) = new_node();
let node = shared_node();
// Act
let gas_limit = node
@@ -753,14 +810,14 @@ mod tests {
.await;
// Assert
let gas_limit = gas_limit.expect("Failed to get the gas limit");
assert_eq!(gas_limit, u32::MAX as u128)
let _ = gas_limit.expect("Failed to get the gas limit");
}
#[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_coinbase_from_node() {
// Arrange
let (_context, node) = new_node();
let node = shared_node();
// Act
let coinbase = node
@@ -771,14 +828,14 @@ mod tests {
.await;
// Assert
let coinbase = coinbase.expect("Failed to get the coinbase");
assert_eq!(coinbase, Address::new([0xFF; 20]))
let _ = coinbase.expect("Failed to get the coinbase");
}
#[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_block_difficulty_from_node() {
// Arrange
let (_context, node) = new_node();
let node = shared_node();
// Act
let block_difficulty = node
@@ -789,14 +846,14 @@ mod tests {
.await;
// Assert
let block_difficulty = block_difficulty.expect("Failed to get the block difficulty");
assert_eq!(block_difficulty, U256::ZERO)
let _ = block_difficulty.expect("Failed to get the block difficulty");
}
#[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_block_hash_from_node() {
// Arrange
let (_context, node) = new_node();
let node = shared_node();
// Act
let block_hash = node
@@ -811,9 +868,10 @@ mod tests {
}
#[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_block_timestamp_from_node() {
// Arrange
let (_context, node) = new_node();
let node = shared_node();
// Act
let block_timestamp = node
@@ -828,15 +886,15 @@ mod tests {
}
#[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_block_number_from_node() {
// Arrange
let (_context, node) = new_node();
let node = shared_node();
// Act
let block_number = node.resolver().await.unwrap().last_block_number().await;
// Assert
let block_number = block_number.expect("Failed to get the block number");
assert_eq!(block_number, 0)
let _ = block_number.expect("Failed to get the block number");
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,4 @@
pub mod geth;
pub mod lighthouse_geth;
pub mod substrate;
pub mod zombienet;
@@ -1,9 +1,8 @@
use std::{
fs::{File, OpenOptions, create_dir_all, remove_dir_all},
io::{BufRead, Write},
path::{Path, PathBuf},
fs::{create_dir_all, remove_dir_all},
path::PathBuf,
pin::Pin,
process::{Child, Command, Stdio},
process::{Command, Stdio},
sync::{
Arc,
atomic::{AtomicU32, Ordering},
@@ -24,17 +23,21 @@ use alloy::{
TxHash, U256,
},
providers::{
Provider, ProviderBuilder,
Provider,
ext::DebugApi,
fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller},
fillers::{CachedNonceManager, ChainIdFiller, NonceFiller},
},
rpc::types::{
EIP1186AccountProofResponse, TransactionReceipt,
EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest,
eth::{Block, Header, Transaction},
trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame},
trace::geth::{
DiffMode, GethDebugTracingOptions, GethTrace, PreStateConfig, PreStateFrame,
},
},
};
use anyhow::Context as _;
use async_stream::stream;
use futures::Stream;
use revive_common::EVMVersion;
use revive_dt_common::fs::clear_directory;
use revive_dt_format::traits::ResolverApi;
@@ -44,10 +47,19 @@ use sp_core::crypto::Ss58Codec;
use sp_runtime::AccountId32;
use revive_dt_config::*;
use revive_dt_node_interaction::EthereumNode;
use revive_dt_node_interaction::{EthereumNode, MinedBlockInformation};
use tokio::sync::OnceCell;
use tracing::instrument;
use crate::{Node, common::FallbackGasFiller, constants::INITIAL_BALANCE};
use crate::{
Node,
constants::{CHAIN_ID, INITIAL_BALANCE},
helpers::{Process, ProcessReadinessWaitBehavior},
provider_utils::{
ConcreteProvider, FallbackGasFiller, construct_concurrency_limited_provider,
execute_transaction,
},
};
static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
@@ -55,6 +67,7 @@ static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
/// or the revive-dev-node which is done by changing the path and some of the other arguments passed
/// to the command.
#[derive(Debug)]
pub struct SubstrateNode {
id: u32,
node_binary: PathBuf,
@@ -63,16 +76,16 @@ pub struct SubstrateNode {
rpc_url: String,
base_directory: PathBuf,
logs_directory: PathBuf,
process_substrate: Option<Child>,
process_proxy: Option<Child>,
substrate_process: Option<Process>,
eth_proxy_process: Option<Process>,
wallet: Arc<EthereumWallet>,
nonce_manager: CachedNonceManager,
chain_id_filler: ChainIdFiller,
logs_file_to_flush: Vec<File>,
provider: OnceCell<ConcreteProvider<ReviveNetwork, Arc<EthereumWallet>>>,
consensus: Option<String>,
}
impl SubstrateNode {
const BASE_DIRECTORY: &str = "Substrate";
const BASE_DIRECTORY: &str = "substrate";
const LOGS_DIRECTORY: &str = "logs";
const DATA_DIRECTORY: &str = "chains";
@@ -85,18 +98,13 @@ impl SubstrateNode {
const SUBSTRATE_LOG_ENV: &str = "error,evm=debug,sc_rpc_server=info,runtime::revive=debug";
const PROXY_LOG_ENV: &str = "info,eth-rpc=debug";
const SUBSTRATE_STDOUT_LOG_FILE_NAME: &str = "node_stdout.log";
const SUBSTRATE_STDERR_LOG_FILE_NAME: &str = "node_stderr.log";
const PROXY_STDOUT_LOG_FILE_NAME: &str = "proxy_stdout.log";
const PROXY_STDERR_LOG_FILE_NAME: &str = "proxy_stderr.log";
pub const KITCHENSINK_EXPORT_CHAINSPEC_COMMAND: &str = "export-chain-spec";
pub const REVIVE_DEV_NODE_EXPORT_CHAINSPEC_COMMAND: &str = "build-spec";
pub fn new(
node_path: PathBuf,
export_chainspec_command: &str,
consensus: Option<String>,
context: impl AsRef<WorkingDirectoryConfiguration>
+ AsRef<EthRpcConfiguration>
+ AsRef<WalletConfiguration>,
@@ -121,16 +129,17 @@ impl SubstrateNode {
rpc_url: String::new(),
base_directory,
logs_directory,
process_substrate: None,
process_proxy: None,
substrate_process: None,
eth_proxy_process: None,
wallet: wallet.clone(),
chain_id_filler: Default::default(),
nonce_manager: Default::default(),
logs_file_to_flush: Vec::with_capacity(4),
provider: Default::default(),
consensus,
}
}
fn init(&mut self, mut genesis: Genesis) -> anyhow::Result<&mut Self> {
let _ = remove_dir_all(self.base_directory.as_path());
let _ = clear_directory(&self.base_directory);
let _ = clear_directory(&self.logs_directory);
@@ -147,6 +156,7 @@ impl SubstrateNode {
.arg(self.export_chainspec_command.as_str())
.arg("--chain")
.arg("dev")
.env_remove("RUST_LOG")
.output()
.context("Failed to export the chain-spec")?;
@@ -213,120 +223,95 @@ impl SubstrateNode {
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;
self.rpc_url = format!("http://127.0.0.1:{proxy_rpc_port}");
let chainspec_path = self.base_directory.join(Self::CHAIN_SPEC_JSON_FILE);
// 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:
// 1. Be consistent
// 2. Less verbose and more dry
// 3. Because the builder pattern uses mutable references so we need to get around that.
let open_options = {
let mut options = OpenOptions::new();
options.create(true).truncate(true).write(true);
options
};
self.rpc_url = format!("http://127.0.0.1:{proxy_rpc_port}");
// Start Substrate node
let substrate_stdout_logs_file = open_options
.clone()
.open(self.substrate_stdout_log_file_path())
.context("Failed to open substrate stdout logs file")?;
let substrate_stderr_logs_file = open_options
.clone()
.open(self.substrate_stderr_log_file_path())
.context("Failed to open substrate stderr logs file")?;
let node_binary_path = self.node_binary.as_path();
self.process_substrate = Command::new(node_binary_path)
.arg("--dev")
.arg("--chain")
.arg(chainspec_path)
.arg("--base-path")
.arg(&self.base_directory)
.arg("--rpc-port")
.arg(substrate_rpc_port.to_string())
.arg("--name")
.arg(format!("revive-substrate-{}", self.id))
.arg("--force-authoring")
.arg("--rpc-methods")
.arg("Unsafe")
.arg("--rpc-cors")
.arg("all")
.arg("--rpc-max-connections")
.arg(u32::MAX.to_string())
.env("RUST_LOG", Self::SUBSTRATE_LOG_ENV)
.stdout(
substrate_stdout_logs_file
.try_clone()
.context("Failed to clone substrate stdout log file handle")?,
)
.stderr(
substrate_stderr_logs_file
.try_clone()
.context("Failed to clone substrate stderr log file handle")?,
)
.spawn()
.context("Failed to spawn Substrate node process")?
.into();
let substrate_process = Process::new(
"node",
self.logs_directory.as_path(),
self.node_binary.as_path(),
|command, stdout_file, stderr_file| {
let cmd = command
.arg("--dev")
.arg("--chain")
.arg(chainspec_path)
.arg("--base-path")
.arg(&self.base_directory)
.arg("--rpc-port")
.arg(substrate_rpc_port.to_string())
.arg("--name")
.arg(format!("revive-substrate-{}", self.id))
.arg("--force-authoring")
.arg("--rpc-methods")
.arg("Unsafe")
.arg("--rpc-cors")
.arg("all")
.arg("--rpc-max-connections")
.arg(u32::MAX.to_string())
.arg("--pool-limit")
.arg(u32::MAX.to_string())
.arg("--pool-kbytes")
.arg(u32::MAX.to_string())
.env("RUST_LOG", Self::SUBSTRATE_LOG_ENV)
.stdout(stdout_file)
.stderr(stderr_file);
if let Some(consensus) = self.consensus.as_ref() {
cmd.arg("--consensus").arg(consensus.clone());
}
},
ProcessReadinessWaitBehavior::TimeBoundedWaitFunction {
max_wait_duration: Duration::from_secs(30),
check_function: Box::new(|_, stderr_line| match stderr_line {
Some(line) => Ok(line.contains(Self::SUBSTRATE_READY_MARKER)),
None => Ok(false),
}),
},
);
match substrate_process {
Ok(process) => self.substrate_process = Some(process),
Err(err) => {
tracing::error!(?err, "Failed to start substrate, shutting down gracefully");
self.shutdown()
.context("Failed to gracefully shutdown after substrate start error")?;
return Err(err);
}
}
// Give the node a moment to boot
if let Err(error) = Self::wait_ready(
self.substrate_stderr_log_file_path().as_path(),
Self::SUBSTRATE_READY_MARKER,
Duration::from_secs(60),
) {
self.shutdown()
.context("Failed to gracefully shutdown after Substrate start error")?;
return Err(error);
};
let eth_proxy_stdout_logs_file = open_options
.clone()
.open(self.proxy_stdout_log_file_path())
.context("Failed to open eth-proxy stdout logs file")?;
let eth_proxy_stderr_logs_file = open_options
.open(self.proxy_stderr_log_file_path())
.context("Failed to open eth-proxy stderr logs file")?;
self.process_proxy = Command::new(&self.eth_proxy_binary)
.arg("--dev")
.arg("--rpc-port")
.arg(proxy_rpc_port.to_string())
.arg("--node-rpc-url")
.arg(format!("ws://127.0.0.1:{substrate_rpc_port}"))
.arg("--rpc-max-connections")
.arg(u32::MAX.to_string())
.env("RUST_LOG", Self::PROXY_LOG_ENV)
.stdout(
eth_proxy_stdout_logs_file
.try_clone()
.context("Failed to clone eth-proxy stdout log file handle")?,
)
.stderr(
eth_proxy_stderr_logs_file
.try_clone()
.context("Failed to clone eth-proxy stderr log file handle")?,
)
.spawn()
.context("Failed to spawn eth-proxy process")?
.into();
if let Err(error) = Self::wait_ready(
self.proxy_stderr_log_file_path().as_path(),
Self::ETH_PROXY_READY_MARKER,
Duration::from_secs(60),
) {
self.shutdown()
.context("Failed to gracefully shutdown after eth-proxy start error")?;
return Err(error);
};
self.logs_file_to_flush.extend([
substrate_stdout_logs_file,
substrate_stderr_logs_file,
eth_proxy_stdout_logs_file,
eth_proxy_stderr_logs_file,
]);
let eth_proxy_process = Process::new(
"proxy",
self.logs_directory.as_path(),
self.eth_proxy_binary.as_path(),
|command, stdout_file, stderr_file| {
command
.arg("--dev")
.arg("--rpc-port")
.arg(proxy_rpc_port.to_string())
.arg("--node-rpc-url")
.arg(format!("ws://127.0.0.1:{substrate_rpc_port}"))
.arg("--rpc-max-connections")
.arg(u32::MAX.to_string())
.env("RUST_LOG", Self::PROXY_LOG_ENV)
.stdout(stdout_file)
.stderr(stderr_file);
},
ProcessReadinessWaitBehavior::TimeBoundedWaitFunction {
max_wait_duration: Duration::from_secs(30),
check_function: Box::new(|_, stderr_line| match stderr_line {
Some(line) => Ok(line.contains(Self::ETH_PROXY_READY_MARKER)),
None => Ok(false),
}),
},
);
match eth_proxy_process {
Ok(process) => self.eth_proxy_process = Some(process),
Err(err) => {
tracing::error!(?err, "Failed to start eth proxy, shutting down gracefully");
self.shutdown()
.context("Failed to gracefully shutdown after eth proxy start error")?;
return Err(err);
}
}
Ok(())
}
@@ -356,29 +341,6 @@ impl SubstrateNode {
account_id.to_ss58check()
}
fn wait_ready(logs_file_path: &Path, marker: &str, timeout: Duration) -> anyhow::Result<()> {
let start_time = std::time::Instant::now();
let logs_file = OpenOptions::new()
.read(true)
.write(false)
.append(false)
.truncate(false)
.open(logs_file_path)?;
let mut lines = std::io::BufReader::new(logs_file).lines();
loop {
if let Some(Ok(line)) = lines.next() {
if line.contains(marker) {
return Ok(());
}
}
if start_time.elapsed() > timeout {
anyhow::bail!("Timeout waiting for process readiness: {marker}");
}
}
}
pub fn eth_rpc_version(&self) -> anyhow::Result<String> {
let output = Command::new(&self.eth_proxy_binary)
.arg("--version")
@@ -391,47 +353,31 @@ impl SubstrateNode {
Ok(String::from_utf8_lossy(&output).trim().to_string())
}
fn substrate_stdout_log_file_path(&self) -> PathBuf {
self.logs_directory
.join(Self::SUBSTRATE_STDOUT_LOG_FILE_NAME)
}
fn substrate_stderr_log_file_path(&self) -> PathBuf {
self.logs_directory
.join(Self::SUBSTRATE_STDERR_LOG_FILE_NAME)
}
fn proxy_stdout_log_file_path(&self) -> PathBuf {
self.logs_directory.join(Self::PROXY_STDOUT_LOG_FILE_NAME)
}
fn proxy_stderr_log_file_path(&self) -> PathBuf {
self.logs_directory.join(Self::PROXY_STDERR_LOG_FILE_NAME)
}
async fn provider(
&self,
) -> anyhow::Result<
FillProvider<impl TxFiller<ReviveNetwork>, impl Provider<ReviveNetwork>, ReviveNetwork>,
> {
ProviderBuilder::new()
.disable_recommended_fillers()
.network::<ReviveNetwork>()
.filler(FallbackGasFiller::new(
25_000_000,
1_000_000_000,
1_000_000_000,
))
.filler(self.chain_id_filler.clone())
.filler(NonceFiller::new(self.nonce_manager.clone()))
.wallet(self.wallet.clone())
.connect(&self.rpc_url)
) -> anyhow::Result<ConcreteProvider<ReviveNetwork, Arc<EthereumWallet>>> {
self.provider
.get_or_try_init(|| async move {
construct_concurrency_limited_provider::<ReviveNetwork, _>(
self.rpc_url.as_str(),
FallbackGasFiller::new(u64::MAX, 5_000_000_000, 1_000_000_000),
ChainIdFiller::new(Some(CHAIN_ID)),
NonceFiller::new(self.nonce_manager.clone()),
self.wallet.clone(),
)
.await
.context("Failed to construct the provider")
})
.await
.map_err(Into::into)
.cloned()
}
}
impl EthereumNode for SubstrateNode {
fn pre_transactions(&mut self) -> Pin<Box<dyn Future<Output = anyhow::Result<()>> + '_>> {
Box::pin(async move { Ok(()) })
}
fn id(&self) -> usize {
self.id as _
}
@@ -440,22 +386,48 @@ impl EthereumNode for SubstrateNode {
&self.rpc_url
}
fn execute_transaction(
fn submit_transaction(
&self,
transaction: alloy::rpc::types::TransactionRequest,
) -> Pin<Box<dyn Future<Output = anyhow::Result<TransactionReceipt>> + '_>> {
transaction: TransactionRequest,
) -> Pin<Box<dyn Future<Output = anyhow::Result<TxHash>> + '_>> {
Box::pin(async move {
let receipt = self
let provider = self
.provider()
.await
.context("Failed to create provider for transaction submission")?
.context("Failed to create the provider for transaction submission")?;
let pending_transaction = provider
.send_transaction(transaction)
.await
.context("Failed to submit transaction to substrate proxy")?
.get_receipt()
.context("Failed to submit the transaction through the provider")?;
Ok(*pending_transaction.tx_hash())
})
}
fn get_receipt(
&self,
tx_hash: TxHash,
) -> Pin<Box<dyn Future<Output = anyhow::Result<TransactionReceipt>> + '_>> {
Box::pin(async move {
self.provider()
.await
.context("Failed to fetch transaction receipt from substrate proxy")?;
Ok(receipt)
.context("Failed to create provider for getting the receipt")?
.get_transaction_receipt(tx_hash)
.await
.context("Failed to get the receipt of the transaction")?
.context("Failed to get the receipt of the transaction")
})
}
fn execute_transaction(
&self,
transaction: TransactionRequest,
) -> Pin<Box<dyn Future<Output = anyhow::Result<TransactionReceipt>> + '_>> {
Box::pin(async move {
let provider = self
.provider()
.await
.context("Failed to create the provider")?;
execute_transaction(provider, transaction).await
})
}
@@ -463,8 +435,7 @@ impl EthereumNode for SubstrateNode {
&self,
tx_hash: TxHash,
trace_options: GethDebugTracingOptions,
) -> Pin<Box<dyn Future<Output = anyhow::Result<alloy::rpc::types::trace::geth::GethTrace>> + '_>>
{
) -> Pin<Box<dyn Future<Output = anyhow::Result<GethTrace>> + '_>> {
Box::pin(async move {
self.provider()
.await
@@ -539,16 +510,65 @@ impl EthereumNode for SubstrateNode {
fn evm_version(&self) -> EVMVersion {
EVMVersion::Cancun
}
fn subscribe_to_full_blocks_information(
&self,
) -> Pin<
Box<
dyn Future<Output = anyhow::Result<Pin<Box<dyn Stream<Item = MinedBlockInformation>>>>>
+ '_,
>,
> {
fn create_stream(
provider: ConcreteProvider<ReviveNetwork, Arc<EthereumWallet>>,
) -> impl Stream<Item = MinedBlockInformation> {
stream! {
let mut block_number = provider.get_block_number().await.expect("Failed to get the block number");
loop {
tokio::time::sleep(Duration::from_secs(1)).await;
let Ok(Some(block)) = provider.get_block_by_number(BlockNumberOrTag::Number(block_number)).await
else {
continue;
};
block_number += 1;
yield MinedBlockInformation {
block_number: block.number(),
block_timestamp: block.header.timestamp,
mined_gas: block.header.gas_used as _,
block_gas_limit: block.header.gas_limit,
transaction_hashes: block
.transactions
.into_hashes()
.as_hashes()
.expect("Must be hashes")
.to_vec(),
};
};
}
}
Box::pin(async move {
let provider = self
.provider()
.await
.context("Failed to create the provider for a block subscription")?;
let stream = Box::pin(create_stream(provider))
as Pin<Box<dyn Stream<Item = MinedBlockInformation>>>;
Ok(stream)
})
}
}
pub struct SubstrateNodeResolver<F: TxFiller<ReviveNetwork>, P: Provider<ReviveNetwork>> {
pub struct SubstrateNodeResolver {
id: u32,
provider: FillProvider<F, P, ReviveNetwork>,
provider: ConcreteProvider<ReviveNetwork, Arc<EthereumWallet>>,
}
impl<F: TxFiller<ReviveNetwork>, P: Provider<ReviveNetwork>> ResolverApi
for SubstrateNodeResolver<F, P>
{
impl ResolverApi for SubstrateNodeResolver {
#[instrument(level = "info", skip_all, fields(substrate_node_id = self.id))]
fn chain_id(
&self,
@@ -673,22 +693,8 @@ impl<F: TxFiller<ReviveNetwork>, P: Provider<ReviveNetwork>> ResolverApi
impl Node for SubstrateNode {
fn shutdown(&mut self) -> anyhow::Result<()> {
// Terminate the processes in a graceful manner to allow for the output to be flushed.
if let Some(mut child) = self.process_proxy.take() {
child
.kill()
.map_err(|error| anyhow::anyhow!("Failed to kill the proxy process: {error:?}"))?;
}
if let Some(mut child) = self.process_substrate.take() {
child.kill().map_err(|error| {
anyhow::anyhow!("Failed to kill the Substrate process: {error:?}")
})?;
}
// Flushing the files that we're using for keeping the logs before shutdown.
for file in self.logs_file_to_flush.iter_mut() {
file.flush()?
}
drop(self.substrate_process.take());
drop(self.eth_proxy_process.take());
// Remove the node's database so that subsequent runs do not run on the same database. We
// ignore the error just in case the directory didn't exist in the first place and therefore
@@ -1158,9 +1164,7 @@ mod tests {
use crate::Node;
fn test_config() -> TestExecutionContext {
let mut context = TestExecutionContext::default();
context.kitchensink_configuration.use_kitchensink = true;
context
TestExecutionContext::default()
}
fn new_node() -> (TestExecutionContext, SubstrateNode) {
@@ -1186,6 +1190,7 @@ mod tests {
let mut node = SubstrateNode::new(
context.kitchensink_configuration.path.clone(),
SubstrateNode::KITCHENSINK_EXPORT_CHAINSPEC_COMMAND,
None,
&context,
);
node.init(context.genesis_configuration.genesis().unwrap().clone())
@@ -1195,19 +1200,19 @@ mod tests {
(context, node)
}
/// A shared node that multiple tests can use. It starts up once.
fn shared_state() -> &'static (TestExecutionContext, SubstrateNode) {
static STATE: LazyLock<(TestExecutionContext, SubstrateNode)> = LazyLock::new(new_node);
&STATE
}
fn shared_node() -> &'static SubstrateNode {
static NODE: LazyLock<(TestExecutionContext, SubstrateNode)> = LazyLock::new(|| {
let (context, node) = new_node();
(context, node)
});
&NODE.1
&shared_state().1
}
#[tokio::test]
async fn node_mines_simple_transfer_transaction_and_returns_receipt() {
// Arrange
let (context, node) = new_node();
let (context, node) = shared_state();
let provider = node.provider().await.expect("Failed to create provider");
@@ -1232,6 +1237,7 @@ mod tests {
}
#[test]
#[ignore = "Ignored since they take a long time to run"]
fn test_init_generates_chainspec_with_balances() {
let genesis_content = r#"
{
@@ -1250,6 +1256,7 @@ mod tests {
let mut dummy_node = SubstrateNode::new(
context.kitchensink_configuration.path.clone(),
SubstrateNode::KITCHENSINK_EXPORT_CHAINSPEC_COMMAND,
None,
&context,
);
@@ -1285,6 +1292,7 @@ mod tests {
}
#[test]
#[ignore = "Ignored since they take a long time to run"]
fn test_parse_genesis_alloc() {
// Create test genesis file
let genesis_json = r#"
@@ -1301,6 +1309,7 @@ mod tests {
let node = SubstrateNode::new(
context.kitchensink_configuration.path.clone(),
SubstrateNode::KITCHENSINK_EXPORT_CHAINSPEC_COMMAND,
None,
&context,
);
@@ -1327,6 +1336,7 @@ mod tests {
}
#[test]
#[ignore = "Ignored since they take a long time to run"]
fn print_eth_to_substrate_mappings() {
let eth_addresses = vec![
"0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1",
@@ -1342,6 +1352,7 @@ mod tests {
}
#[test]
#[ignore = "Ignored since they take a long time to run"]
fn test_eth_to_substrate_address() {
let cases = vec![
(
@@ -1372,6 +1383,7 @@ mod tests {
}
#[test]
#[ignore = "Ignored since they take a long time to run"]
fn version_works() {
let node = shared_node();
@@ -1384,6 +1396,7 @@ mod tests {
}
#[test]
#[ignore = "Ignored since they take a long time to run"]
fn eth_rpc_version_works() {
let node = shared_node();
@@ -1396,6 +1409,7 @@ mod tests {
}
#[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_chain_id_from_node() {
// Arrange
let node = shared_node();
@@ -1409,6 +1423,7 @@ mod tests {
}
#[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_gas_limit_from_node() {
// Arrange
let node = shared_node();
@@ -1426,6 +1441,7 @@ mod tests {
}
#[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_coinbase_from_node() {
// Arrange
let node = shared_node();
@@ -1443,6 +1459,7 @@ mod tests {
}
#[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_block_difficulty_from_node() {
// Arrange
let node = shared_node();
@@ -1460,6 +1477,7 @@ mod tests {
}
#[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_block_hash_from_node() {
// Arrange
let node = shared_node();
@@ -1477,6 +1495,7 @@ mod tests {
}
#[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_block_timestamp_from_node() {
// Arrange
let node = shared_node();
@@ -1494,6 +1513,7 @@ mod tests {
}
#[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn can_get_block_number_from_node() {
// Arrange
let node = shared_node();
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,69 @@
use std::sync::Arc;
use alloy::transports::BoxFuture;
use tokio::sync::Semaphore;
use tower::{Layer, Service};
#[derive(Clone, Debug)]
pub struct ConcurrencyLimiterLayer {
semaphore: Arc<Semaphore>,
}
impl ConcurrencyLimiterLayer {
pub fn new(permit_count: usize) -> Self {
Self {
semaphore: Arc::new(Semaphore::new(permit_count)),
}
}
}
impl<S> Layer<S> for ConcurrencyLimiterLayer {
type Service = ConcurrencyLimiterService<S>;
fn layer(&self, inner: S) -> Self::Service {
ConcurrencyLimiterService {
service: inner,
semaphore: self.semaphore.clone(),
}
}
}
#[derive(Clone)]
pub struct ConcurrencyLimiterService<S> {
service: S,
semaphore: Arc<Semaphore>,
}
impl<S, Request> Service<Request> for ConcurrencyLimiterService<S>
where
S: Service<Request> + Send,
S::Future: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
fn poll_ready(
&mut self,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}
fn call(&mut self, req: Request) -> Self::Future {
let semaphore = self.semaphore.clone();
let future = self.service.call(req);
Box::pin(async move {
let _permit = semaphore
.acquire()
.await
.expect("Semaphore has been closed");
tracing::debug!(
available_permits = semaphore.available_permits(),
"Acquired Semaphore Permit"
);
future.await
})
}
}
@@ -7,6 +7,10 @@ use alloy::{
transports::TransportResult,
};
// Percentage padding applied to estimated gas (e.g. 120 = 20% padding)
const GAS_ESTIMATE_PADDING_NUMERATOR: u64 = 120;
const GAS_ESTIMATE_PADDING_DENOMINATOR: u64 = 100;
#[derive(Clone, Debug)]
pub struct FallbackGasFiller {
inner: GasFiller,
@@ -30,6 +34,12 @@ impl FallbackGasFiller {
}
}
impl Default for FallbackGasFiller {
fn default() -> Self {
FallbackGasFiller::new(25_000_000, 1_000_000_000, 1_000_000_000)
}
}
impl<N> TxFiller<N> for FallbackGasFiller
where
N: Network,
@@ -50,8 +60,6 @@ where
provider: &P,
tx: &<N as Network>::TransactionRequest,
) -> TransportResult<Self::Fillable> {
// Try to fetch GasFillers “fillable” (gas_price, base_fee, estimate_gas, …)
// If it errors (i.e. tx would revert under eth_estimateGas), swallow it.
match self.inner.prepare(provider, tx).await {
Ok(fill) => Ok(Some(fill)),
Err(_) => Ok(None),
@@ -64,8 +72,17 @@ where
mut tx: alloy::providers::SendableTx<N>,
) -> TransportResult<SendableTx<N>> {
if let Some(fill) = fillable {
// our inner GasFiller succeeded — use it
self.inner.fill(fill, tx).await
let mut tx = self.inner.fill(fill, tx).await?;
if let Some(builder) = tx.as_mut_builder() {
if let Some(estimated) = builder.gas_limit() {
let padded = estimated
.checked_mul(GAS_ESTIMATE_PADDING_NUMERATOR)
.and_then(|v| v.checked_div(GAS_ESTIMATE_PADDING_DENOMINATOR))
.unwrap_or(u64::MAX);
builder.set_gas_limit(padded);
}
}
Ok(tx)
} else {
if let Some(builder) = tx.as_mut_builder() {
builder.set_gas_limit(self.default_gas_limit);
+7
View File
@@ -0,0 +1,7 @@
mod concurrency_limiter;
mod fallback_gas_filler;
mod provider;
pub use concurrency_limiter::*;
pub use fallback_gas_filler::*;
pub use provider::*;
+132
View File
@@ -0,0 +1,132 @@
use std::{ops::ControlFlow, sync::LazyLock, time::Duration};
use alloy::{
network::{Ethereum, Network, NetworkWallet, TransactionBuilder4844},
providers::{
Identity, PendingTransactionBuilder, Provider, ProviderBuilder, RootProvider,
fillers::{ChainIdFiller, FillProvider, JoinFill, NonceFiller, TxFiller, WalletFiller},
},
rpc::client::ClientBuilder,
};
use anyhow::{Context, Result};
use revive_dt_common::futures::{PollingWaitBehavior, poll};
use tracing::{Instrument, debug, info, info_span};
use crate::provider_utils::{ConcurrencyLimiterLayer, FallbackGasFiller};
pub type ConcreteProvider<N, W> = FillProvider<
JoinFill<
JoinFill<JoinFill<JoinFill<Identity, FallbackGasFiller>, ChainIdFiller>, NonceFiller>,
WalletFiller<W>,
>,
RootProvider<N>,
N,
>;
pub async fn construct_concurrency_limited_provider<N, W>(
rpc_url: &str,
fallback_gas_filler: FallbackGasFiller,
chain_id_filler: ChainIdFiller,
nonce_filler: NonceFiller,
wallet: W,
) -> Result<ConcreteProvider<N, W>>
where
N: Network<TransactionRequest: TransactionBuilder4844>,
W: NetworkWallet<N>,
Identity: TxFiller<N>,
FallbackGasFiller: TxFiller<N>,
ChainIdFiller: TxFiller<N>,
NonceFiller: TxFiller<N>,
WalletFiller<W>: TxFiller<N>,
{
// This is a global limit on the RPC concurrency that applies to all of the providers created
// by the framework. With this limit, it means that we can have a maximum of N concurrent
// requests at any point of time and no more than that. This is done in an effort to stabilize
// the framework from some of the interment issues that we've been seeing related to RPC calls.
static GLOBAL_CONCURRENCY_LIMITER_LAYER: LazyLock<ConcurrencyLimiterLayer> =
LazyLock::new(|| ConcurrencyLimiterLayer::new(500));
let client = ClientBuilder::default()
.layer(GLOBAL_CONCURRENCY_LIMITER_LAYER.clone())
.connect(rpc_url)
.await
.context("Failed to construct the RPC client")?;
let provider = ProviderBuilder::new()
.disable_recommended_fillers()
.network::<N>()
.filler(fallback_gas_filler)
.filler(chain_id_filler)
.filler(nonce_filler)
.wallet(wallet)
.connect_client(client);
Ok(provider)
}
pub async fn execute_transaction<N, W>(
provider: ConcreteProvider<N, W>,
transaction: N::TransactionRequest,
) -> Result<N::ReceiptResponse>
where
N: Network<
TransactionRequest: TransactionBuilder4844,
TxEnvelope = <Ethereum as Network>::TxEnvelope,
>,
W: NetworkWallet<N>,
Identity: TxFiller<N>,
FallbackGasFiller: TxFiller<N>,
ChainIdFiller: TxFiller<N>,
NonceFiller: TxFiller<N>,
WalletFiller<W>: TxFiller<N>,
{
let sendable_transaction = provider
.fill(transaction)
.await
.context("Failed to fill transaction")?;
let transaction_envelope = sendable_transaction
.try_into_envelope()
.context("Failed to convert transaction into an envelope")?;
let tx_hash = *transaction_envelope.tx_hash();
let mut pending_transaction = match provider.send_tx_envelope(transaction_envelope).await {
Ok(pending_transaction) => pending_transaction,
Err(error) => {
let error_string = error.to_string();
if error_string.contains("Transaction Already Imported") {
PendingTransactionBuilder::<N>::new(provider.root().clone(), tx_hash)
} else {
return Err(error).context(format!("Failed to submit transaction {tx_hash}"));
}
}
};
debug!(%tx_hash, "Submitted Transaction");
pending_transaction.set_timeout(Some(Duration::from_secs(120)));
let tx_hash = pending_transaction.watch().await.context(format!(
"Transaction inclusion watching timeout for {tx_hash}"
))?;
poll(
Duration::from_secs(60),
PollingWaitBehavior::Constant(Duration::from_secs(3)),
|| {
let provider = provider.clone();
async move {
match provider.get_transaction_receipt(tx_hash).await {
Ok(Some(receipt)) => {
info!("Found the transaction receipt");
Ok(ControlFlow::Break(receipt))
}
_ => Ok(ControlFlow::Continue(())),
}
}
},
)
.instrument(info_span!("Polling for receipt", %tx_hash))
.await
.context(format!("Polling for receipt failed for {tx_hash}"))
}
+1 -1
View File
@@ -13,7 +13,7 @@ revive-dt-config = { workspace = true }
revive-dt-format = { workspace = true }
revive-dt-compiler = { workspace = true }
alloy-primitives = { workspace = true }
alloy = { workspace = true }
anyhow = { workspace = true }
paste = { workspace = true }
indexmap = { workspace = true, features = ["serde"] }
+9 -1
View File
@@ -8,7 +8,7 @@ use std::{
time::{SystemTime, UNIX_EPOCH},
};
use alloy_primitives::Address;
use alloy::primitives::Address;
use anyhow::{Context as _, Result};
use indexmap::IndexMap;
use revive_dt_common::types::PlatformIdentifier;
@@ -106,6 +106,10 @@ impl ReportAggregator {
RunnerEvent::ContractDeployed(event) => {
self.handle_contract_deployed_event(*event);
}
RunnerEvent::Completion(event) => {
self.handle_completion(*event);
break;
}
}
}
debug!("Report aggregation completed");
@@ -382,6 +386,10 @@ impl ReportAggregator {
.insert(event.contract_instance, event.address);
}
fn handle_completion(&mut self, _: CompletionEvent) {
self.runner_rx.close();
}
fn test_case_report(&mut self, specifier: &TestSpecifier) -> &mut TestCaseReport {
self.report
.test_case_information
+2 -2
View File
@@ -4,7 +4,7 @@ use std::{path::PathBuf, sync::Arc};
use revive_dt_common::{define_wrapper_type, types::PlatformIdentifier};
use revive_dt_compiler::Mode;
use revive_dt_format::{case::CaseIdx, input::StepIdx};
use revive_dt_format::{case::CaseIdx, steps::StepPath};
use serde::{Deserialize, Serialize};
define_wrapper_type!(
@@ -33,5 +33,5 @@ pub struct ExecutionSpecifier {
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct StepExecutionSpecifier {
pub execution_specifier: Arc<ExecutionSpecifier>,
pub step_idx: StepIdx,
pub step_idx: StepPath,
}
+3 -1
View File
@@ -3,7 +3,7 @@
use std::{collections::BTreeMap, path::PathBuf, sync::Arc};
use alloy_primitives::Address;
use alloy::primitives::Address;
use anyhow::Context as _;
use indexmap::IndexMap;
use revive_dt_common::types::PlatformIdentifier;
@@ -613,6 +613,8 @@ define_event! {
/// The address of the contract.
address: Address
},
/// Reports the completion of the run.
Completion {}
}
}
+10 -6
View File
@@ -75,7 +75,9 @@ ABSOLUTE_PATH=$(realpath "$TEST_REPO_DIR/fixtures/solidity/")
cat > "$CORPUS_FILE" << EOF
{
"name": "MatterLabs Solidity Simple, Complex, and Semantic Tests",
"path": "$ABSOLUTE_PATH"
"paths": [
"$(realpath "$TEST_REPO_DIR/fixtures/solidity/simple")"
]
}
EOF
@@ -89,16 +91,18 @@ echo "This may take a while..."
echo ""
# Run the tool
RUST_LOG="info" cargo run --release -- execute-tests \
cargo build --release;
RUST_LOG="info,alloy_pubsub::service=error" ./target/release/retester test \
--platform geth-evm-solc \
--platform revive-dev-node-polkavm-resolc \
--corpus "$CORPUS_FILE" \
--working-directory "$WORKDIR" \
--concurrency.number-of-nodes 5 \
--concurrency.number-of-nodes 10 \
--concurrency.number-of-threads 5 \
--concurrency.ignore-concurrency-limit \
--wallet.additional-keys 100000 \
--kitchensink.path "$SUBSTRATE_NODE_BIN" \
--revive-dev-node.path "$REVIVE_DEV_NODE_BIN" \
--eth-rpc.path "$ETH_RPC_BIN" \
> logs.log \
2> output.log
> logs.log
echo -e "${GREEN}=== Test run completed! ===${NC}"
+43 -5
View File
@@ -201,6 +201,10 @@
{
"description": "A special step for repeating a bunch of steps a certain number of times.",
"$ref": "#/$defs/RepeatStep"
},
{
"description": "A step type that allows for a new account address to be allocated and to later on be used\nas the caller in another step.",
"$ref": "#/$defs/AllocateAccountStep"
}
]
},
@@ -377,9 +381,13 @@
"properties": {
"address": {
"description": "An optional field of the address of the emitter of the event.",
"type": [
"string",
"null"
"anyOf": [
{
"$ref": "#/$defs/StepAddress"
},
{
"type": "null"
}
]
},
"topics": {
@@ -399,6 +407,10 @@
"values"
]
},
"StepAddress": {
"description": "An address type that might either be an address literal or a resolvable address.",
"type": "string"
},
"EtherValue": {
"description": "Defines an Ether value.\n\nThis is an unsigned 256 bit integer that's followed by some denomination which can either be\neth, ether, gwei, or wei.",
"type": "string"
@@ -431,7 +443,7 @@
},
"address": {
"description": "The address that the balance assertion should be done on.\n\nThis is a string which will be resolved into an address when being processed. Therefore,\nthis could be a normal hex address, a variable such as `Test.address`, or perhaps even a\nfull on variable like `$VARIABLE:Uniswap`. It follows the same resolution rules that are\nfollowed in the calldata.",
"type": "string"
"$ref": "#/$defs/StepAddress"
},
"expected_balance": {
"description": "The amount of balance to assert that the account or contract has. This is a 256 bit string\nthat's serialized and deserialized into a decimal string.",
@@ -456,7 +468,7 @@
},
"address": {
"description": "The address that the balance assertion should be done on.\n\nThis is a string which will be resolved into an address when being processed. Therefore,\nthis could be a normal hex address, a variable such as `Test.address`, or perhaps even a\nfull on variable like `$VARIABLE:Uniswap`. It follows the same resolution rules that are\nfollowed in the calldata.",
"type": "string"
"$ref": "#/$defs/StepAddress"
},
"is_storage_empty": {
"description": "A boolean of whether the storage of the address is empty or not.",
@@ -472,6 +484,13 @@
"description": "This represents a repetition step which is a special step type that allows for a sequence of\nsteps to be repeated (on different drivers) a certain number of times.",
"type": "object",
"properties": {
"comment": {
"description": "An optional comment on the repetition step.",
"type": [
"string",
"null"
]
},
"repeat": {
"description": "The number of repetitions that the steps should be repeated for.",
"type": "integer",
@@ -491,6 +510,25 @@
"steps"
]
},
"AllocateAccountStep": {
"type": "object",
"properties": {
"comment": {
"description": "An optional comment on the account allocation step.",
"type": [
"string",
"null"
]
},
"allocate_account": {
"description": "An instruction to allocate a new account with the value being the variable name of that\naccount. This must start with `$VARIABLE:` and then be followed by the variable name of the\naccount.",
"type": "string"
}
},
"required": [
"allocate_account"
]
},
"ContractPathAndIdent": {
"description": "Represents an identifier used for contracts.\n\nThe type supports serialization from and into the following string format:\n\n```text\n${path}:${contract_ident}\n```",
"type": "string"