mirror of
https://github.com/pezkuwichain/revive-differential-tests.git
synced 2026-04-22 10:17:56 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a28a90168 | |||
| ee55eea6b2 | |||
| 1026426616 | |||
| 2827f32478 | |||
| 3c86cbb7ef | |||
| fde07b7c0d | |||
| ebc24a588b | |||
| 21e25f09e6 | |||
| 8c412dc924 | |||
| 6da3172581 | |||
| c6eb04b04e | |||
| e5114d31dc | |||
| 74fdeb4a2e | |||
| f9dc362c03 |
@@ -15,6 +15,7 @@ concurrency:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
POLKADOT_VERSION: polkadot-stable2506-2
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cache-polkadot:
|
cache-polkadot:
|
||||||
@@ -65,6 +66,37 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cd polkadot-sdk
|
cd polkadot-sdk
|
||||||
cargo install --path substrate/frame/revive/rpc --bin eth-rpc
|
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:
|
ci:
|
||||||
name: CI on ${{ matrix.os }}
|
name: CI on ${{ matrix.os }}
|
||||||
@@ -86,15 +118,33 @@ jobs:
|
|||||||
~/.cargo/bin/eth-rpc
|
~/.cargo/bin/eth-rpc
|
||||||
key: polkadot-binaries-${{ matrix.os }}-${{ hashFiles('polkadot-sdk/.git') }}
|
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
|
- name: Setup Rust toolchain
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
rustflags: ""
|
rustflags: ""
|
||||||
|
|
||||||
- name: Add wasm32 target
|
- name: Add wasm32 target and formatting
|
||||||
run: |
|
run: |
|
||||||
rustup target add wasm32-unknown-unknown
|
rustup target add wasm32-unknown-unknown
|
||||||
rustup component add rust-src
|
rustup component add rust-src rustfmt clippy
|
||||||
|
|
||||||
- name: Install Geth on Ubuntu
|
- name: Install Geth on Ubuntu
|
||||||
if: matrix.os == 'ubuntu-24.04'
|
if: matrix.os == 'ubuntu-24.04'
|
||||||
@@ -141,6 +191,17 @@ jobs:
|
|||||||
chmod +x resolc
|
chmod +x resolc
|
||||||
sudo mv resolc /usr/local/bin
|
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
|
- name: Machete
|
||||||
uses: bnjbvr/cargo-machete@v0.7.1
|
uses: bnjbvr/cargo-machete@v0.7.1
|
||||||
|
|
||||||
@@ -159,5 +220,34 @@ jobs:
|
|||||||
- name: Check resolc version
|
- name: Check resolc version
|
||||||
run: resolc --version
|
run: resolc --version
|
||||||
|
|
||||||
- name: Test cargo workspace
|
- name: Check polkadot version
|
||||||
run: make test
|
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::
|
||||||
|
|||||||
Generated
+2587
-114
File diff suppressed because it is too large
Load Diff
+11
-4
@@ -22,8 +22,7 @@ revive-dt-node-pool = { version = "0.1.0", path = "crates/node-pool" }
|
|||||||
revive-dt-report = { version = "0.1.0", path = "crates/report" }
|
revive-dt-report = { version = "0.1.0", path = "crates/report" }
|
||||||
revive-dt-solc-binaries = { version = "0.1.0", path = "crates/solc-binaries" }
|
revive-dt-solc-binaries = { version = "0.1.0", path = "crates/solc-binaries" }
|
||||||
|
|
||||||
alloy-primitives = "1.2.1"
|
ansi_term = "0.12.1"
|
||||||
alloy-sol-types = "1.2.1"
|
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
bson = { version = "2.15.0" }
|
bson = { version = "2.15.0" }
|
||||||
cacache = { version = "13.1.0" }
|
cacache = { version = "13.1.0" }
|
||||||
@@ -45,11 +44,13 @@ serde_json = { version = "1.0", default-features = false, features = [
|
|||||||
"std",
|
"std",
|
||||||
"unbounded_depth",
|
"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" }
|
sha2 = { version = "0.10.9" }
|
||||||
sp-core = "36.1.0"
|
sp-core = "36.1.0"
|
||||||
sp-runtime = "41.1.0"
|
sp-runtime = "41.1.0"
|
||||||
strum = { version = "0.27.2", features = ["derive"] }
|
strum = { version = "0.27.2", features = ["derive"] }
|
||||||
|
subxt = { version = "0.44.0" }
|
||||||
temp-dir = { version = "0.1.16" }
|
temp-dir = { version = "0.1.16" }
|
||||||
tempfile = "3.3"
|
tempfile = "3.3"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
@@ -58,6 +59,7 @@ tokio = { version = "1.47.0", default-features = false, features = [
|
|||||||
"process",
|
"process",
|
||||||
"rt",
|
"rt",
|
||||||
] }
|
] }
|
||||||
|
tower = { version = "0.5.2", features = ["limit"] }
|
||||||
uuid = { version = "1.8", features = ["v4"] }
|
uuid = { version = "1.8", features = ["v4"] }
|
||||||
tracing = { version = "0.1.41" }
|
tracing = { version = "0.1.41" }
|
||||||
tracing-appender = { version = "0.2.3" }
|
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-common = { git = "https://github.com/paritytech/revive", rev = "3389865af7c3ff6f29a586d82157e8bc573c1a8e" }
|
||||||
revive-differential = { 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]
|
[workspace.dependencies.alloy]
|
||||||
version = "1.0.22"
|
version = "1.0.37"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = [
|
features = [
|
||||||
"json-abi",
|
"json-abi",
|
||||||
"providers",
|
"providers",
|
||||||
|
"provider-ws",
|
||||||
"provider-ipc",
|
"provider-ipc",
|
||||||
|
"provider-http",
|
||||||
"provider-debug-api",
|
"provider-debug-api",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rpc-types",
|
"rpc-types",
|
||||||
@@ -89,6 +95,7 @@ features = [
|
|||||||
"serde",
|
"serde",
|
||||||
"rpc-types-eth",
|
"rpc-types-eth",
|
||||||
"genesis",
|
"genesis",
|
||||||
|
"sol-types",
|
||||||
]
|
]
|
||||||
|
|
||||||
[profile.bench]
|
[profile.bench]
|
||||||
|
|||||||
@@ -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.
|
- 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.
|
- 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.
|
- 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.
|
All of the above need to be installed and available in the path in order for the tool to work.
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -10,11 +10,11 @@ rust-version.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
alloy = { workspace = true }
|
alloy = { workspace = true }
|
||||||
alloy-primitives = { workspace = true }
|
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
clap = { workspace = true }
|
clap = { workspace = true }
|
||||||
moka = { workspace = true, features = ["sync"] }
|
moka = { workspace = true, features = ["sync"] }
|
||||||
once_cell = { workspace = true }
|
once_cell = { workspace = true }
|
||||||
|
regex = { workspace = true }
|
||||||
semver = { workspace = true }
|
semver = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
schemars = { workspace = true }
|
schemars = { workspace = true }
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ use strum::{AsRefStr, Display, EnumString, IntoStaticStr};
|
|||||||
pub enum PlatformIdentifier {
|
pub enum PlatformIdentifier {
|
||||||
/// The Go-ethereum reference full node EVM implementation with the solc compiler.
|
/// The Go-ethereum reference full node EVM implementation with the solc compiler.
|
||||||
GethEvmSolc,
|
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.
|
/// The kitchensink node with the PolkaVM backend with the resolc compiler.
|
||||||
KitchensinkPolkavmResolc,
|
KitchensinkPolkavmResolc,
|
||||||
/// The kitchensink node with the REVM backend with the solc compiler.
|
/// The kitchensink node with the REVM backend with the solc compiler.
|
||||||
@@ -37,6 +39,10 @@ pub enum PlatformIdentifier {
|
|||||||
ReviveDevNodePolkavmResolc,
|
ReviveDevNodePolkavmResolc,
|
||||||
/// The revive dev node with the REVM backend with the solc compiler.
|
/// The revive dev node with the REVM backend with the solc compiler.
|
||||||
ReviveDevNodeRevmSolc,
|
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.
|
/// 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 {
|
pub enum NodeIdentifier {
|
||||||
/// The go-ethereum node implementation.
|
/// The go-ethereum node implementation.
|
||||||
Geth,
|
Geth,
|
||||||
|
/// The go-ethereum node implementation.
|
||||||
|
LighthouseGeth,
|
||||||
/// The Kitchensink node implementation.
|
/// The Kitchensink node implementation.
|
||||||
Kitchensink,
|
Kitchensink,
|
||||||
/// The revive dev node implementation.
|
/// The revive dev node implementation.
|
||||||
ReviveDevNode,
|
ReviveDevNode,
|
||||||
|
/// A zombienet spawned nodes
|
||||||
|
Zombienet,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An enum representing the identifiers of the supported VMs.
|
/// An enum representing the identifiers of the supported VMs.
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
mod identifiers;
|
mod identifiers;
|
||||||
mod mode;
|
mod mode;
|
||||||
mod private_key_allocator;
|
mod private_key_allocator;
|
||||||
|
mod round_robin_pool;
|
||||||
mod version_or_requirement;
|
mod version_or_requirement;
|
||||||
|
|
||||||
pub use identifiers::*;
|
pub use identifiers::*;
|
||||||
pub use mode::*;
|
pub use mode::*;
|
||||||
pub use private_key_allocator::*;
|
pub use private_key_allocator::*;
|
||||||
|
pub use round_robin_pool::*;
|
||||||
pub use version_or_requirement::*;
|
pub use version_or_requirement::*;
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
|
use crate::iterators::EitherIter;
|
||||||
use crate::types::VersionOrRequirement;
|
use crate::types::VersionOrRequirement;
|
||||||
|
use anyhow::{Context as _, bail};
|
||||||
|
use regex::Regex;
|
||||||
|
use schemars::JsonSchema;
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashSet;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
@@ -33,6 +38,19 @@ impl Display for Mode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FromStr for Mode {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let parsed_mode = ParsedMode::from_str(s)?;
|
||||||
|
let mut iter = parsed_mode.to_modes();
|
||||||
|
let (Some(mode), None) = (iter.next(), iter.next()) else {
|
||||||
|
bail!("Failed to parse the mode")
|
||||||
|
};
|
||||||
|
Ok(mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Mode {
|
impl Mode {
|
||||||
/// Return all of the available mode combinations.
|
/// Return all of the available mode combinations.
|
||||||
pub fn all() -> impl Iterator<Item = &'static Mode> {
|
pub fn all() -> impl Iterator<Item = &'static Mode> {
|
||||||
@@ -171,3 +189,250 @@ impl ModeOptimizerSetting {
|
|||||||
!matches!(self, ModeOptimizerSetting::M0)
|
!matches!(self, ModeOptimizerSetting::M0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This represents a mode that has been parsed from test metadata.
|
||||||
|
///
|
||||||
|
/// Mode strings can take the following form (in pseudo-regex):
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// [YEILV][+-]? (M[0123sz])? <semver>?
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// We can parse valid mode strings into [`ParsedMode`] using [`ParsedMode::from_str`].
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)]
|
||||||
|
#[serde(try_from = "String", into = "String")]
|
||||||
|
pub struct ParsedMode {
|
||||||
|
pub pipeline: Option<ModePipeline>,
|
||||||
|
pub optimize_flag: Option<bool>,
|
||||||
|
pub optimize_setting: Option<ModeOptimizerSetting>,
|
||||||
|
pub version: Option<semver::VersionReq>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ParsedMode {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
static REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||||
|
Regex::new(r"(?x)
|
||||||
|
^
|
||||||
|
(?:(?P<pipeline>[YEILV])(?P<optimize_flag>[+-])?)? # Pipeline to use eg Y, E+, E-
|
||||||
|
\s*
|
||||||
|
(?P<optimize_setting>M[a-zA-Z0-9])? # Optimize setting eg M0, Ms, Mz
|
||||||
|
\s*
|
||||||
|
(?P<version>[>=<^]*\d+(?:\.\d+)*)? # Optional semver version eg >=0.8.0, 0.7, <0.8
|
||||||
|
$
|
||||||
|
").unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
let Some(caps) = REGEX.captures(s) else {
|
||||||
|
anyhow::bail!("Cannot parse mode '{s}' from string");
|
||||||
|
};
|
||||||
|
|
||||||
|
let pipeline = match caps.name("pipeline") {
|
||||||
|
Some(m) => Some(
|
||||||
|
ModePipeline::from_str(m.as_str())
|
||||||
|
.context("Failed to parse mode pipeline from string")?,
|
||||||
|
),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let optimize_flag = caps.name("optimize_flag").map(|m| m.as_str() == "+");
|
||||||
|
|
||||||
|
let optimize_setting = match caps.name("optimize_setting") {
|
||||||
|
Some(m) => Some(
|
||||||
|
ModeOptimizerSetting::from_str(m.as_str())
|
||||||
|
.context("Failed to parse optimizer setting from string")?,
|
||||||
|
),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let version = match caps.name("version") {
|
||||||
|
Some(m) => Some(
|
||||||
|
semver::VersionReq::parse(m.as_str())
|
||||||
|
.map_err(|e| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"Cannot parse the version requirement '{}': {e}",
|
||||||
|
m.as_str()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.context("Failed to parse semver requirement from mode string")?,
|
||||||
|
),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ParsedMode {
|
||||||
|
pipeline,
|
||||||
|
optimize_flag,
|
||||||
|
optimize_setting,
|
||||||
|
version,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ParsedMode {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let mut has_written = false;
|
||||||
|
|
||||||
|
if let Some(pipeline) = self.pipeline {
|
||||||
|
pipeline.fmt(f)?;
|
||||||
|
if let Some(optimize_flag) = self.optimize_flag {
|
||||||
|
f.write_str(if optimize_flag { "+" } else { "-" })?;
|
||||||
|
}
|
||||||
|
has_written = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(optimize_setting) = self.optimize_setting {
|
||||||
|
if has_written {
|
||||||
|
f.write_str(" ")?;
|
||||||
|
}
|
||||||
|
optimize_setting.fmt(f)?;
|
||||||
|
has_written = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(version) = &self.version {
|
||||||
|
if has_written {
|
||||||
|
f.write_str(" ")?;
|
||||||
|
}
|
||||||
|
version.fmt(f)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ParsedMode> for String {
|
||||||
|
fn from(parsed_mode: ParsedMode) -> Self {
|
||||||
|
parsed_mode.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<String> for ParsedMode {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||||
|
ParsedMode::from_str(&value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParsedMode {
|
||||||
|
/// This takes a [`ParsedMode`] and expands it into a list of [`Mode`]s that we should try.
|
||||||
|
pub fn to_modes(&self) -> impl Iterator<Item = Mode> {
|
||||||
|
let pipeline_iter = self.pipeline.as_ref().map_or_else(
|
||||||
|
|| EitherIter::A(ModePipeline::test_cases()),
|
||||||
|
|p| EitherIter::B(std::iter::once(*p)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let optimize_flag_setting = self.optimize_flag.map(|flag| {
|
||||||
|
if flag {
|
||||||
|
ModeOptimizerSetting::M3
|
||||||
|
} else {
|
||||||
|
ModeOptimizerSetting::M0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let optimize_flag_iter = match optimize_flag_setting {
|
||||||
|
Some(setting) => EitherIter::A(std::iter::once(setting)),
|
||||||
|
None => EitherIter::B(ModeOptimizerSetting::test_cases()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let optimize_settings_iter = self.optimize_setting.as_ref().map_or_else(
|
||||||
|
|| EitherIter::A(optimize_flag_iter),
|
||||||
|
|s| EitherIter::B(std::iter::once(*s)),
|
||||||
|
);
|
||||||
|
|
||||||
|
pipeline_iter.flat_map(move |pipeline| {
|
||||||
|
optimize_settings_iter
|
||||||
|
.clone()
|
||||||
|
.map(move |optimize_setting| Mode {
|
||||||
|
pipeline,
|
||||||
|
optimize_setting,
|
||||||
|
version: self.version.clone(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a set of [`Mode`]s that correspond to the given [`ParsedMode`]s.
|
||||||
|
/// This avoids any duplicate entries.
|
||||||
|
pub fn many_to_modes<'a>(
|
||||||
|
parsed: impl Iterator<Item = &'a ParsedMode>,
|
||||||
|
) -> impl Iterator<Item = Mode> {
|
||||||
|
let modes: HashSet<_> = parsed.flat_map(|p| p.to_modes()).collect();
|
||||||
|
modes.into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parsed_mode_from_str() {
|
||||||
|
let strings = vec![
|
||||||
|
("Mz", "Mz"),
|
||||||
|
("Y", "Y"),
|
||||||
|
("Y+", "Y+"),
|
||||||
|
("Y-", "Y-"),
|
||||||
|
("E", "E"),
|
||||||
|
("E+", "E+"),
|
||||||
|
("E-", "E-"),
|
||||||
|
("Y M0", "Y M0"),
|
||||||
|
("Y M1", "Y M1"),
|
||||||
|
("Y M2", "Y M2"),
|
||||||
|
("Y M3", "Y M3"),
|
||||||
|
("Y Ms", "Y Ms"),
|
||||||
|
("Y Mz", "Y Mz"),
|
||||||
|
("E M0", "E M0"),
|
||||||
|
("E M1", "E M1"),
|
||||||
|
("E M2", "E M2"),
|
||||||
|
("E M3", "E M3"),
|
||||||
|
("E Ms", "E Ms"),
|
||||||
|
("E Mz", "E Mz"),
|
||||||
|
// When stringifying semver again, 0.8.0 becomes ^0.8.0 (same meaning)
|
||||||
|
("Y 0.8.0", "Y ^0.8.0"),
|
||||||
|
("E+ 0.8.0", "E+ ^0.8.0"),
|
||||||
|
("Y M3 >=0.8.0", "Y M3 >=0.8.0"),
|
||||||
|
("E Mz <0.7.0", "E Mz <0.7.0"),
|
||||||
|
// We can parse +- _and_ M1/M2 but the latter takes priority.
|
||||||
|
("Y+ M1 0.8.0", "Y+ M1 ^0.8.0"),
|
||||||
|
("E- M2 0.7.0", "E- M2 ^0.7.0"),
|
||||||
|
// We don't see this in the wild but it is parsed.
|
||||||
|
("<=0.8", "<=0.8"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (actual, expected) in strings {
|
||||||
|
let parsed = ParsedMode::from_str(actual)
|
||||||
|
.unwrap_or_else(|_| panic!("Failed to parse mode string '{actual}'"));
|
||||||
|
assert_eq!(
|
||||||
|
expected,
|
||||||
|
parsed.to_string(),
|
||||||
|
"Mode string '{actual}' did not parse to '{expected}': got '{parsed}'"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parsed_mode_to_test_modes() {
|
||||||
|
let strings = vec![
|
||||||
|
("Mz", vec!["Y Mz", "E Mz"]),
|
||||||
|
("Y", vec!["Y M0", "Y M3"]),
|
||||||
|
("E", vec!["E M0", "E M3"]),
|
||||||
|
("Y+", vec!["Y M3"]),
|
||||||
|
("Y-", vec!["Y M0"]),
|
||||||
|
("Y <=0.8", vec!["Y M0 <=0.8", "Y M3 <=0.8"]),
|
||||||
|
(
|
||||||
|
"<=0.8",
|
||||||
|
vec!["Y M0 <=0.8", "Y M3 <=0.8", "E M0 <=0.8", "E M3 <=0.8"],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (actual, expected) in strings {
|
||||||
|
let parsed = ParsedMode::from_str(actual)
|
||||||
|
.unwrap_or_else(|_| panic!("Failed to parse mode string '{actual}'"));
|
||||||
|
let expected_set: HashSet<_> = expected.into_iter().map(|s| s.to_owned()).collect();
|
||||||
|
let actual_set: HashSet<_> = parsed.to_modes().map(|m| m.to_string()).collect();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
expected_set, actual_set,
|
||||||
|
"Mode string '{actual}' did not expand to '{expected_set:?}': got '{actual_set:?}'"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
use alloy::primitives::U256;
|
||||||
use alloy::signers::local::PrivateKeySigner;
|
use alloy::signers::local::PrivateKeySigner;
|
||||||
use alloy_primitives::U256;
|
use anyhow::{Context, Result, bail};
|
||||||
use anyhow::{Result, bail};
|
|
||||||
|
|
||||||
/// This is a sequential private key allocator. When instantiated, it allocated private keys in
|
/// 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.
|
/// sequentially and in order until the maximum private key specified is reached.
|
||||||
@@ -10,25 +10,26 @@ pub struct PrivateKeyAllocator {
|
|||||||
next_private_key: U256,
|
next_private_key: U256,
|
||||||
|
|
||||||
/// The highest private key (exclusive) that can be returned by this allocator.
|
/// The highest private key (exclusive) that can be returned by this allocator.
|
||||||
highest_private_key_exclusive: U256,
|
highest_private_key_inclusive: U256,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PrivateKeyAllocator {
|
impl PrivateKeyAllocator {
|
||||||
/// Creates a new instance of the private key allocator.
|
/// Creates a new instance of the private key allocator.
|
||||||
pub fn new(highest_private_key_exclusive: U256) -> Self {
|
pub fn new(highest_private_key_inclusive: U256) -> Self {
|
||||||
Self {
|
Self {
|
||||||
next_private_key: U256::ZERO,
|
next_private_key: U256::ONE,
|
||||||
highest_private_key_exclusive,
|
highest_private_key_inclusive,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Allocates a new private key and errors out if the maximum private key has been reached.
|
/// Allocates a new private key and errors out if the maximum private key has been reached.
|
||||||
pub fn allocate(&mut self) -> Result<PrivateKeySigner> {
|
pub fn allocate(&mut self) -> Result<PrivateKeySigner> {
|
||||||
if self.next_private_key >= self.highest_private_key_exclusive {
|
if self.next_private_key > self.highest_private_key_inclusive {
|
||||||
bail!("Attempted to allocate a private key but failed since all have been allocated");
|
bail!("Attempted to allocate a private key but failed since all have been allocated");
|
||||||
};
|
};
|
||||||
let private_key =
|
let private_key =
|
||||||
PrivateKeySigner::from_slice(self.next_private_key.to_be_bytes::<32>().as_slice())?;
|
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;
|
self.next_private_key += U256::ONE;
|
||||||
Ok(private_key)
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,6 @@ revive-dt-solc-binaries = { workspace = true }
|
|||||||
revive-common = { workspace = true }
|
revive-common = { workspace = true }
|
||||||
|
|
||||||
alloy = { workspace = true }
|
alloy = { workspace = true }
|
||||||
alloy-primitives = { workspace = true }
|
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
dashmap = { workspace = true }
|
dashmap = { workspace = true }
|
||||||
foundry-compilers-artifacts = { workspace = true }
|
foundry-compilers-artifacts = { workspace = true }
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use alloy::json_abi::JsonAbi;
|
use alloy::json_abi::JsonAbi;
|
||||||
use alloy_primitives::Address;
|
use alloy::primitives::Address;
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use revive_solc_json_interface::{
|
|||||||
SolcStandardJsonInputSettingsOptimizer, SolcStandardJsonInputSettingsSelection,
|
SolcStandardJsonInputSettingsOptimizer, SolcStandardJsonInputSettingsSelection,
|
||||||
SolcStandardJsonOutput,
|
SolcStandardJsonOutput,
|
||||||
};
|
};
|
||||||
|
use tracing::{Span, field::display};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler, solc::Solc,
|
CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler, solc::Solc,
|
||||||
@@ -80,6 +81,16 @@ impl SolidityCompiler for Resolc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug", ret)]
|
#[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(
|
fn build(
|
||||||
&self,
|
&self,
|
||||||
CompilerInput {
|
CompilerInput {
|
||||||
@@ -141,6 +152,7 @@ impl SolidityCompiler for Resolc {
|
|||||||
polkavm: None,
|
polkavm: None,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
Span::current().record("json_in", display(serde_json::to_string(&input).unwrap()));
|
||||||
|
|
||||||
let path = &self.0.resolc_path;
|
let path = &self.0.resolc_path;
|
||||||
let mut command = AsyncCommand::new(path);
|
let mut command = AsyncCommand::new(path);
|
||||||
@@ -148,6 +160,8 @@ impl SolidityCompiler for Resolc {
|
|||||||
.stdin(Stdio::piped())
|
.stdin(Stdio::piped())
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::piped())
|
.stderr(Stdio::piped())
|
||||||
|
.arg("--solc")
|
||||||
|
.arg(self.0.solc.path())
|
||||||
.arg("--standard-json");
|
.arg("--standard-json");
|
||||||
|
|
||||||
if let Some(ref base_path) = base_path {
|
if let Some(ref base_path) = base_path {
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ use std::{
|
|||||||
|
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use revive_dt_common::types::VersionOrRequirement;
|
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 revive_dt_solc_binaries::download_solc;
|
||||||
|
use tracing::{Span, field::display, info};
|
||||||
|
|
||||||
use crate::{CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler};
|
use crate::{CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler};
|
||||||
|
|
||||||
@@ -39,9 +40,7 @@ struct SolcInner {
|
|||||||
|
|
||||||
impl Solc {
|
impl Solc {
|
||||||
pub async fn new(
|
pub async fn new(
|
||||||
context: impl AsRef<SolcConfiguration>
|
context: impl AsRef<SolcConfiguration> + AsRef<WorkingDirectoryConfiguration>,
|
||||||
+ AsRef<ResolcConfiguration>
|
|
||||||
+ AsRef<WorkingDirectoryConfiguration>,
|
|
||||||
version: impl Into<Option<VersionOrRequirement>>,
|
version: impl Into<Option<VersionOrRequirement>>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
// This is a cache for the compiler objects so that whenever the same compiler version is
|
// 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
|
Ok(COMPILERS_CACHE
|
||||||
.entry((path.clone(), version.clone()))
|
.entry((path.clone(), version.clone()))
|
||||||
.or_insert_with(|| {
|
.or_insert_with(|| {
|
||||||
|
info!(
|
||||||
|
solc_path = %path.display(),
|
||||||
|
solc_version = %version,
|
||||||
|
"Created a new solc compiler object"
|
||||||
|
);
|
||||||
Self(Arc::new(SolcInner {
|
Self(Arc::new(SolcInner {
|
||||||
solc_path: path,
|
solc_path: path,
|
||||||
solc_version: version,
|
solc_version: version,
|
||||||
@@ -88,6 +92,12 @@ impl SolidityCompiler for Solc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug", ret)]
|
#[tracing::instrument(level = "debug", ret)]
|
||||||
|
#[tracing::instrument(
|
||||||
|
level = "error",
|
||||||
|
skip_all,
|
||||||
|
fields(json_in = tracing::field::Empty),
|
||||||
|
err(Debug)
|
||||||
|
)]
|
||||||
fn build(
|
fn build(
|
||||||
&self,
|
&self,
|
||||||
CompilerInput {
|
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 path = &self.0.solc_path;
|
||||||
let mut command = AsyncCommand::new(path);
|
let mut command = AsyncCommand::new(path);
|
||||||
command
|
command
|
||||||
.stdin(Stdio::piped())
|
.stdin(Stdio::piped())
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::piped())
|
.stderr(Stdio::null())
|
||||||
.arg("--standard-json");
|
.arg("--standard-json");
|
||||||
|
|
||||||
if let Some(ref base_path) = base_path {
|
if let Some(ref base_path) = base_path {
|
||||||
@@ -205,20 +217,18 @@ impl SolidityCompiler for Solc {
|
|||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let json_in = serde_json::to_string_pretty(&input)
|
let json_in = serde_json::to_string_pretty(&input)
|
||||||
.context("Failed to pretty-print Standard JSON input for logging")?;
|
.context("Failed to pretty-print Standard JSON input for logging")?;
|
||||||
let message = String::from_utf8_lossy(&output.stderr);
|
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
status = %output.status,
|
status = %output.status,
|
||||||
message = %message,
|
|
||||||
json_input = json_in,
|
json_input = json_in,
|
||||||
"Compilation using solc failed"
|
"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)
|
let parsed = serde_json::from_slice::<SolcOutput>(&output.stdout)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
anyhow::anyhow!(
|
anyhow::anyhow!(
|
||||||
"failed to parse resolc JSON output: {e}\nstderr: {}",
|
"failed to parse resolc JSON output: {e}\nstdout: {}",
|
||||||
String::from_utf8_lossy(&output.stdout)
|
String::from_utf8_lossy(&output.stdout)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ pragma solidity >=0.6.9;
|
|||||||
import "./callable.sol";
|
import "./callable.sol";
|
||||||
|
|
||||||
contract Main {
|
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);
|
return callable.f(p1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+427
-69
@@ -12,23 +12,26 @@ use std::{
|
|||||||
|
|
||||||
use alloy::{
|
use alloy::{
|
||||||
genesis::Genesis,
|
genesis::Genesis,
|
||||||
hex::ToHexExt,
|
|
||||||
network::EthereumWallet,
|
network::EthereumWallet,
|
||||||
primitives::{FixedBytes, U256},
|
primitives::{B256, FixedBytes, U256},
|
||||||
signers::local::PrivateKeySigner,
|
signers::local::PrivateKeySigner,
|
||||||
};
|
};
|
||||||
use clap::{Parser, ValueEnum, ValueHint};
|
use clap::{Parser, ValueEnum, ValueHint};
|
||||||
use revive_dt_common::types::PlatformIdentifier;
|
use revive_dt_common::types::PlatformIdentifier;
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use serde::{Serialize, Serializer};
|
use serde::{Deserialize, Serialize, Serializer};
|
||||||
use strum::{AsRefStr, Display, EnumString, IntoStaticStr};
|
use strum::{AsRefStr, Display, EnumString, IntoStaticStr};
|
||||||
use temp_dir::TempDir;
|
use temp_dir::TempDir;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Parser, Serialize)]
|
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
|
||||||
#[command(name = "retester")]
|
#[command(name = "retester")]
|
||||||
pub enum Context {
|
pub enum Context {
|
||||||
/// Executes tests in the MatterLabs format differentially on multiple targets concurrently.
|
/// 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.
|
/// Exports the JSON schema of the MatterLabs test format used by the tool.
|
||||||
ExportJsonSchema,
|
ExportJsonSchema,
|
||||||
}
|
}
|
||||||
@@ -46,7 +49,18 @@ impl Context {
|
|||||||
impl AsRef<WorkingDirectoryConfiguration> for Context {
|
impl AsRef<WorkingDirectoryConfiguration> for Context {
|
||||||
fn as_ref(&self) -> &WorkingDirectoryConfiguration {
|
fn as_ref(&self) -> &WorkingDirectoryConfiguration {
|
||||||
match self {
|
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!(),
|
Self::ExportJsonSchema => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,7 +69,8 @@ impl AsRef<WorkingDirectoryConfiguration> for Context {
|
|||||||
impl AsRef<SolcConfiguration> for Context {
|
impl AsRef<SolcConfiguration> for Context {
|
||||||
fn as_ref(&self) -> &SolcConfiguration {
|
fn as_ref(&self) -> &SolcConfiguration {
|
||||||
match self {
|
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!(),
|
Self::ExportJsonSchema => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,7 +79,8 @@ impl AsRef<SolcConfiguration> for Context {
|
|||||||
impl AsRef<ResolcConfiguration> for Context {
|
impl AsRef<ResolcConfiguration> for Context {
|
||||||
fn as_ref(&self) -> &ResolcConfiguration {
|
fn as_ref(&self) -> &ResolcConfiguration {
|
||||||
match self {
|
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!(),
|
Self::ExportJsonSchema => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,7 +89,28 @@ impl AsRef<ResolcConfiguration> for Context {
|
|||||||
impl AsRef<GethConfiguration> for Context {
|
impl AsRef<GethConfiguration> for Context {
|
||||||
fn as_ref(&self) -> &GethConfiguration {
|
fn as_ref(&self) -> &GethConfiguration {
|
||||||
match self {
|
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!(),
|
Self::ExportJsonSchema => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,7 +119,8 @@ impl AsRef<GethConfiguration> for Context {
|
|||||||
impl AsRef<KitchensinkConfiguration> for Context {
|
impl AsRef<KitchensinkConfiguration> for Context {
|
||||||
fn as_ref(&self) -> &KitchensinkConfiguration {
|
fn as_ref(&self) -> &KitchensinkConfiguration {
|
||||||
match self {
|
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!(),
|
Self::ExportJsonSchema => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,7 +129,8 @@ impl AsRef<KitchensinkConfiguration> for Context {
|
|||||||
impl AsRef<ReviveDevNodeConfiguration> for Context {
|
impl AsRef<ReviveDevNodeConfiguration> for Context {
|
||||||
fn as_ref(&self) -> &ReviveDevNodeConfiguration {
|
fn as_ref(&self) -> &ReviveDevNodeConfiguration {
|
||||||
match self {
|
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!(),
|
Self::ExportJsonSchema => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,7 +139,8 @@ impl AsRef<ReviveDevNodeConfiguration> for Context {
|
|||||||
impl AsRef<EthRpcConfiguration> for Context {
|
impl AsRef<EthRpcConfiguration> for Context {
|
||||||
fn as_ref(&self) -> &EthRpcConfiguration {
|
fn as_ref(&self) -> &EthRpcConfiguration {
|
||||||
match self {
|
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!(),
|
Self::ExportJsonSchema => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,7 +149,11 @@ impl AsRef<EthRpcConfiguration> for Context {
|
|||||||
impl AsRef<GenesisConfiguration> for Context {
|
impl AsRef<GenesisConfiguration> for Context {
|
||||||
fn as_ref(&self) -> &GenesisConfiguration {
|
fn as_ref(&self) -> &GenesisConfiguration {
|
||||||
match self {
|
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!(),
|
Self::ExportJsonSchema => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,7 +162,8 @@ impl AsRef<GenesisConfiguration> for Context {
|
|||||||
impl AsRef<WalletConfiguration> for Context {
|
impl AsRef<WalletConfiguration> for Context {
|
||||||
fn as_ref(&self) -> &WalletConfiguration {
|
fn as_ref(&self) -> &WalletConfiguration {
|
||||||
match self {
|
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!(),
|
Self::ExportJsonSchema => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,7 +172,8 @@ impl AsRef<WalletConfiguration> for Context {
|
|||||||
impl AsRef<ConcurrencyConfiguration> for Context {
|
impl AsRef<ConcurrencyConfiguration> for Context {
|
||||||
fn as_ref(&self) -> &ConcurrencyConfiguration {
|
fn as_ref(&self) -> &ConcurrencyConfiguration {
|
||||||
match self {
|
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!(),
|
Self::ExportJsonSchema => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,7 +182,8 @@ impl AsRef<ConcurrencyConfiguration> for Context {
|
|||||||
impl AsRef<CompilationConfiguration> for Context {
|
impl AsRef<CompilationConfiguration> for Context {
|
||||||
fn as_ref(&self) -> &CompilationConfiguration {
|
fn as_ref(&self) -> &CompilationConfiguration {
|
||||||
match self {
|
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!(),
|
Self::ExportJsonSchema => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,14 +192,37 @@ impl AsRef<CompilationConfiguration> for Context {
|
|||||||
impl AsRef<ReportConfiguration> for Context {
|
impl AsRef<ReportConfiguration> for Context {
|
||||||
fn as_ref(&self) -> &ReportConfiguration {
|
fn as_ref(&self) -> &ReportConfiguration {
|
||||||
match self {
|
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!(),
|
Self::ExportJsonSchema => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Parser, Serialize)]
|
impl AsRef<IgnoreSuccessConfiguration> for Context {
|
||||||
|
fn as_ref(&self) -> &IgnoreSuccessConfiguration {
|
||||||
|
match self {
|
||||||
|
Self::Test(context) => context.as_ref().as_ref(),
|
||||||
|
Self::Benchmark(..) => unreachable!(),
|
||||||
|
Self::ExportJsonSchema => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
|
||||||
pub struct TestExecutionContext {
|
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
|
/// The working directory that the program will use for all of the temporary artifacts needed at
|
||||||
/// runtime.
|
/// runtime.
|
||||||
///
|
///
|
||||||
@@ -166,17 +236,9 @@ pub struct TestExecutionContext {
|
|||||||
)]
|
)]
|
||||||
pub working_directory: WorkingDirectoryConfiguration,
|
pub working_directory: WorkingDirectoryConfiguration,
|
||||||
|
|
||||||
/// The set of platforms that the differential tests should run on.
|
/// Configuration parameters for the corpus files to use.
|
||||||
#[arg(
|
#[clap(flatten, next_help_heading = "Corpus Configuration")]
|
||||||
short = 'p',
|
pub corpus_configuration: CorpusConfiguration,
|
||||||
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 solc compiler.
|
/// Configuration parameters for the solc compiler.
|
||||||
#[clap(flatten, next_help_heading = "Solc Configuration")]
|
#[clap(flatten, next_help_heading = "Solc Configuration")]
|
||||||
@@ -186,10 +248,18 @@ pub struct TestExecutionContext {
|
|||||||
#[clap(flatten, next_help_heading = "Resolc Configuration")]
|
#[clap(flatten, next_help_heading = "Resolc Configuration")]
|
||||||
pub resolc_configuration: ResolcConfiguration,
|
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.
|
/// Configuration parameters for the geth node.
|
||||||
#[clap(flatten, next_help_heading = "Geth Configuration")]
|
#[clap(flatten, next_help_heading = "Geth Configuration")]
|
||||||
pub geth_configuration: GethConfiguration,
|
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.
|
/// Configuration parameters for the Kitchensink.
|
||||||
#[clap(flatten, next_help_heading = "Kitchensink Configuration")]
|
#[clap(flatten, next_help_heading = "Kitchensink Configuration")]
|
||||||
pub kitchensink_configuration: KitchensinkConfiguration,
|
pub kitchensink_configuration: KitchensinkConfiguration,
|
||||||
@@ -221,6 +291,91 @@ pub struct TestExecutionContext {
|
|||||||
/// Configuration parameters for the report.
|
/// Configuration parameters for the report.
|
||||||
#[clap(flatten, next_help_heading = "Report Configuration")]
|
#[clap(flatten, next_help_heading = "Report Configuration")]
|
||||||
pub report_configuration: ReportConfiguration,
|
pub report_configuration: ReportConfiguration,
|
||||||
|
|
||||||
|
/// Configuration parameters for ignoring certain test cases based on the report
|
||||||
|
#[clap(flatten, next_help_heading = "Ignore Success Configuration")]
|
||||||
|
pub ignore_success_configuration: IgnoreSuccessConfiguration,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
|
||||||
|
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 {
|
impl Default for TestExecutionContext {
|
||||||
@@ -235,6 +390,12 @@ impl AsRef<WorkingDirectoryConfiguration> for TestExecutionContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AsRef<CorpusConfiguration> for TestExecutionContext {
|
||||||
|
fn as_ref(&self) -> &CorpusConfiguration {
|
||||||
|
&self.corpus_configuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl AsRef<SolcConfiguration> for TestExecutionContext {
|
impl AsRef<SolcConfiguration> for TestExecutionContext {
|
||||||
fn as_ref(&self) -> &SolcConfiguration {
|
fn as_ref(&self) -> &SolcConfiguration {
|
||||||
&self.solc_configuration
|
&self.solc_configuration
|
||||||
@@ -253,6 +414,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 {
|
impl AsRef<KitchensinkConfiguration> for TestExecutionContext {
|
||||||
fn as_ref(&self) -> &KitchensinkConfiguration {
|
fn as_ref(&self) -> &KitchensinkConfiguration {
|
||||||
&self.kitchensink_configuration
|
&self.kitchensink_configuration
|
||||||
@@ -301,8 +474,112 @@ impl AsRef<ReportConfiguration> for TestExecutionContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AsRef<IgnoreSuccessConfiguration> for TestExecutionContext {
|
||||||
|
fn as_ref(&self) -> &IgnoreSuccessConfiguration {
|
||||||
|
&self.ignore_success_configuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, Deserialize)]
|
||||||
|
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.
|
/// A set of configuration parameters for Solc.
|
||||||
#[derive(Clone, Debug, Parser, Serialize)]
|
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
|
||||||
pub struct SolcConfiguration {
|
pub struct SolcConfiguration {
|
||||||
/// Specifies the default version of the Solc compiler that should be used if there is no
|
/// Specifies the default version of the Solc compiler that should be used if there is no
|
||||||
/// override specified by one of the test cases.
|
/// override specified by one of the test cases.
|
||||||
@@ -311,7 +588,7 @@ pub struct SolcConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A set of configuration parameters for Resolc.
|
/// A set of configuration parameters for Resolc.
|
||||||
#[derive(Clone, Debug, Parser, Serialize)]
|
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
|
||||||
pub struct ResolcConfiguration {
|
pub struct ResolcConfiguration {
|
||||||
/// Specifies the path of the resolc compiler to be used by the tool.
|
/// Specifies the path of the resolc compiler to be used by the tool.
|
||||||
///
|
///
|
||||||
@@ -321,8 +598,32 @@ pub struct ResolcConfiguration {
|
|||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A set of configuration parameters for Polkadot Parachain.
|
||||||
|
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
|
||||||
|
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.
|
/// A set of configuration parameters for Geth.
|
||||||
#[derive(Clone, Debug, Parser, Serialize)]
|
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
|
||||||
pub struct GethConfiguration {
|
pub struct GethConfiguration {
|
||||||
/// Specifies the path of the geth node to be used by the tool.
|
/// Specifies the path of the geth node to be used by the tool.
|
||||||
///
|
///
|
||||||
@@ -335,14 +636,29 @@ pub struct GethConfiguration {
|
|||||||
#[clap(
|
#[clap(
|
||||||
id = "geth.start-timeout-ms",
|
id = "geth.start-timeout-ms",
|
||||||
long = "geth.start-timeout-ms",
|
long = "geth.start-timeout-ms",
|
||||||
default_value = "5000",
|
default_value = "30000",
|
||||||
value_parser = parse_duration
|
value_parser = parse_duration
|
||||||
)]
|
)]
|
||||||
pub start_timeout_ms: Duration,
|
pub start_timeout_ms: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A set of configuration parameters for kurtosis.
|
||||||
|
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
|
||||||
|
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.
|
/// A set of configuration parameters for Kitchensink.
|
||||||
#[derive(Clone, Debug, Parser, Serialize)]
|
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
|
||||||
pub struct KitchensinkConfiguration {
|
pub struct KitchensinkConfiguration {
|
||||||
/// Specifies the path of the kitchensink node to be used by the tool.
|
/// Specifies the path of the kitchensink node to be used by the tool.
|
||||||
///
|
///
|
||||||
@@ -359,18 +675,14 @@ pub struct KitchensinkConfiguration {
|
|||||||
#[clap(
|
#[clap(
|
||||||
id = "kitchensink.start-timeout-ms",
|
id = "kitchensink.start-timeout-ms",
|
||||||
long = "kitchensink.start-timeout-ms",
|
long = "kitchensink.start-timeout-ms",
|
||||||
default_value = "5000",
|
default_value = "30000",
|
||||||
value_parser = parse_duration
|
value_parser = parse_duration
|
||||||
)]
|
)]
|
||||||
pub start_timeout_ms: 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.
|
/// A set of configuration parameters for the revive dev node.
|
||||||
#[derive(Clone, Debug, Parser, Serialize)]
|
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
|
||||||
pub struct ReviveDevNodeConfiguration {
|
pub struct ReviveDevNodeConfiguration {
|
||||||
/// Specifies the path of the revive dev node to be used by the tool.
|
/// Specifies the path of the revive dev node to be used by the tool.
|
||||||
///
|
///
|
||||||
@@ -387,14 +699,22 @@ pub struct ReviveDevNodeConfiguration {
|
|||||||
#[clap(
|
#[clap(
|
||||||
id = "revive-dev-node.start-timeout-ms",
|
id = "revive-dev-node.start-timeout-ms",
|
||||||
long = "revive-dev-node.start-timeout-ms",
|
long = "revive-dev-node.start-timeout-ms",
|
||||||
default_value = "5000",
|
default_value = "30000",
|
||||||
value_parser = parse_duration
|
value_parser = parse_duration
|
||||||
)]
|
)]
|
||||||
pub start_timeout_ms: 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.
|
/// A set of configuration parameters for the ETH RPC.
|
||||||
#[derive(Clone, Debug, Parser, Serialize)]
|
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
|
||||||
pub struct EthRpcConfiguration {
|
pub struct EthRpcConfiguration {
|
||||||
/// Specifies the path of the ETH RPC to be used by the tool.
|
/// Specifies the path of the ETH RPC to be used by the tool.
|
||||||
///
|
///
|
||||||
@@ -407,14 +727,14 @@ pub struct EthRpcConfiguration {
|
|||||||
#[clap(
|
#[clap(
|
||||||
id = "eth-rpc.start-timeout-ms",
|
id = "eth-rpc.start-timeout-ms",
|
||||||
long = "eth-rpc.start-timeout-ms",
|
long = "eth-rpc.start-timeout-ms",
|
||||||
default_value = "5000",
|
default_value = "30000",
|
||||||
value_parser = parse_duration
|
value_parser = parse_duration
|
||||||
)]
|
)]
|
||||||
pub start_timeout_ms: Duration,
|
pub start_timeout_ms: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A set of configuration parameters for the genesis.
|
/// A set of configuration parameters for the genesis.
|
||||||
#[derive(Clone, Debug, Parser, Serialize)]
|
#[derive(Clone, Debug, Default, Parser, Serialize, Deserialize)]
|
||||||
pub struct GenesisConfiguration {
|
pub struct GenesisConfiguration {
|
||||||
/// Specifies the path of the genesis file to use for the nodes that are started.
|
/// Specifies the path of the genesis file to use for the nodes that are started.
|
||||||
///
|
///
|
||||||
@@ -431,7 +751,7 @@ pub struct GenesisConfiguration {
|
|||||||
impl GenesisConfiguration {
|
impl GenesisConfiguration {
|
||||||
pub fn genesis(&self) -> anyhow::Result<&Genesis> {
|
pub fn genesis(&self) -> anyhow::Result<&Genesis> {
|
||||||
static DEFAULT_GENESIS: LazyLock<Genesis> = LazyLock::new(|| {
|
static DEFAULT_GENESIS: LazyLock<Genesis> = LazyLock::new(|| {
|
||||||
let genesis = include_str!("../../../dev-genesis.json");
|
let genesis = include_str!("../../../assets/dev-genesis.json");
|
||||||
serde_json::from_str(genesis).unwrap()
|
serde_json::from_str(genesis).unwrap()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -452,21 +772,20 @@ impl GenesisConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A set of configuration parameters for the wallet.
|
/// A set of configuration parameters for the wallet.
|
||||||
#[derive(Clone, Debug, Parser, Serialize)]
|
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
|
||||||
pub struct WalletConfiguration {
|
pub struct WalletConfiguration {
|
||||||
/// The private key of the default signer.
|
/// The private key of the default signer.
|
||||||
#[clap(
|
#[clap(
|
||||||
long = "wallet.default-private-key",
|
long = "wallet.default-private-key",
|
||||||
default_value = "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d"
|
default_value = "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d"
|
||||||
)]
|
)]
|
||||||
#[serde(serialize_with = "serialize_private_key")]
|
default_key: B256,
|
||||||
default_key: PrivateKeySigner,
|
|
||||||
|
|
||||||
/// This argument controls which private keys the nodes should have access to and be added to
|
/// This argument controls which private keys the nodes should have access to and be added to
|
||||||
/// its wallet signers. With a value of N, private keys (0, N] will be added to the signer set
|
/// its wallet signers. With a value of N, private keys (0, N] will be added to the signer set
|
||||||
/// of the node.
|
/// of the node.
|
||||||
#[clap(long = "wallet.additional-keys", default_value_t = 100_000)]
|
#[clap(long = "wallet.additional-keys", default_value_t = 100_000)]
|
||||||
additional_keys: usize,
|
pub additional_keys: usize,
|
||||||
|
|
||||||
/// The wallet object that will be used.
|
/// The wallet object that will be used.
|
||||||
#[clap(skip)]
|
#[clap(skip)]
|
||||||
@@ -478,7 +797,8 @@ impl WalletConfiguration {
|
|||||||
pub fn wallet(&self) -> Arc<EthereumWallet> {
|
pub fn wallet(&self) -> Arc<EthereumWallet> {
|
||||||
self.wallet
|
self.wallet
|
||||||
.get_or_init(|| {
|
.get_or_init(|| {
|
||||||
let mut wallet = EthereumWallet::new(self.default_key.clone());
|
let mut wallet =
|
||||||
|
EthereumWallet::new(PrivateKeySigner::from_bytes(&self.default_key).unwrap());
|
||||||
for signer in (1..=self.additional_keys)
|
for signer in (1..=self.additional_keys)
|
||||||
.map(|id| U256::from(id))
|
.map(|id| U256::from(id))
|
||||||
.map(|id| id.to_be_bytes::<32>())
|
.map(|id| id.to_be_bytes::<32>())
|
||||||
@@ -496,15 +816,8 @@ impl WalletConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serialize_private_key<S>(value: &PrivateKeySigner, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
value.to_bytes().encode_hex().serialize(serializer)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A set of configuration for concurrency.
|
/// A set of configuration for concurrency.
|
||||||
#[derive(Clone, Debug, Parser, Serialize)]
|
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
|
||||||
pub struct ConcurrencyConfiguration {
|
pub struct ConcurrencyConfiguration {
|
||||||
/// Determines the amount of nodes that will be spawned for each chain.
|
/// Determines the amount of nodes that will be spawned for each chain.
|
||||||
#[clap(long = "concurrency.number-of-nodes", default_value_t = 5)]
|
#[clap(long = "concurrency.number-of-nodes", default_value_t = 5)]
|
||||||
@@ -542,14 +855,14 @@ impl ConcurrencyConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Parser, Serialize)]
|
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
|
||||||
pub struct CompilationConfiguration {
|
pub struct CompilationConfiguration {
|
||||||
/// Controls if the compilation cache should be invalidated or not.
|
/// Controls if the compilation cache should be invalidated or not.
|
||||||
#[arg(long = "compilation.invalidate-cache")]
|
#[arg(long = "compilation.invalidate-cache")]
|
||||||
pub invalidate_compilation_cache: bool,
|
pub invalidate_compilation_cache: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Parser, Serialize)]
|
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
|
||||||
pub struct ReportConfiguration {
|
pub struct ReportConfiguration {
|
||||||
/// Controls if the compiler input is included in the final report.
|
/// Controls if the compiler input is included in the final report.
|
||||||
#[clap(long = "report.include-compiler-input")]
|
#[clap(long = "report.include-compiler-input")]
|
||||||
@@ -560,6 +873,13 @@ pub struct ReportConfiguration {
|
|||||||
pub include_compiler_output: bool,
|
pub include_compiler_output: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
|
||||||
|
pub struct IgnoreSuccessConfiguration {
|
||||||
|
/// The path of the report generated by the tool to use to ignore the cases that succeeded.
|
||||||
|
#[clap(long = "ignore-success.report-path")]
|
||||||
|
pub path: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Represents the working directory that the program uses.
|
/// Represents the working directory that the program uses.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum WorkingDirectoryConfiguration {
|
pub enum WorkingDirectoryConfiguration {
|
||||||
@@ -569,6 +889,24 @@ pub enum WorkingDirectoryConfiguration {
|
|||||||
Path(PathBuf),
|
Path(PathBuf),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Serialize for WorkingDirectoryConfiguration {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
self.as_path().serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Deserialize<'a> for WorkingDirectoryConfiguration {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'a>,
|
||||||
|
{
|
||||||
|
PathBuf::deserialize(deserializer).map(Self::Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl WorkingDirectoryConfiguration {
|
impl WorkingDirectoryConfiguration {
|
||||||
pub fn as_path(&self) -> &Path {
|
pub fn as_path(&self) -> &Path {
|
||||||
self.as_ref()
|
self.as_ref()
|
||||||
@@ -618,15 +956,6 @@ impl Display for WorkingDirectoryConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Serialize for WorkingDirectoryConfiguration {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::Serializer,
|
|
||||||
{
|
|
||||||
self.as_path().serialize(serializer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_duration(s: &str) -> anyhow::Result<Duration> {
|
fn parse_duration(s: &str) -> anyhow::Result<Duration> {
|
||||||
u64::from_str(s)
|
u64::from_str(s)
|
||||||
.map(Duration::from_millis)
|
.map(Duration::from_millis)
|
||||||
@@ -658,4 +987,33 @@ pub enum TestingPlatform {
|
|||||||
Geth,
|
Geth,
|
||||||
/// The kitchensink runtime provides the PolkaVM (PVM) based node implementation.
|
/// The kitchensink runtime provides the PolkaVM (PVM) based node implementation.
|
||||||
Kitchensink,
|
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,
|
||||||
|
Deserialize,
|
||||||
|
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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ revive-dt-node = { workspace = true }
|
|||||||
revive-dt-node-interaction = { workspace = true }
|
revive-dt-node-interaction = { workspace = true }
|
||||||
revive-dt-report = { workspace = true }
|
revive-dt-report = { workspace = true }
|
||||||
|
|
||||||
|
ansi_term = { workspace = true }
|
||||||
alloy = { workspace = true }
|
alloy = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
bson = { 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,181 @@
|
|||||||
|
//! 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,
|
||||||
|
None,
|
||||||
|
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,201 @@
|
|||||||
|
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,ref_time,max_ref_time,proof_size,max_proof_size"
|
||||||
|
);
|
||||||
|
// 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(),
|
||||||
|
block.ref_time,
|
||||||
|
block.max_ref_time,
|
||||||
|
block.proof_size,
|
||||||
|
block.max_proof_size,
|
||||||
|
);
|
||||||
|
// 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,358 @@
|
|||||||
|
//! 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::{cached_fs::read_to_string, 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 only_execute_failed_tests = match context.ignore_success_configuration.path.as_ref() {
|
||||||
|
Some(path) => {
|
||||||
|
let report = read_to_string(path)
|
||||||
|
.context("Failed to read the report file to ignore the succeeding test cases")?;
|
||||||
|
Some(serde_json::from_str(&report).context("Failed to deserialize report")?)
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
let full_context = Context::Test(Box::new(context.clone()));
|
||||||
|
let test_definitions = create_test_definitions_stream(
|
||||||
|
&full_context,
|
||||||
|
metadata_files.iter(),
|
||||||
|
&platforms_and_nodes,
|
||||||
|
only_execute_failed_tests.as_ref(),
|
||||||
|
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();
|
||||||
|
|
||||||
|
buf = tokio::task::spawn_blocking(move || {
|
||||||
|
buf.flush().unwrap();
|
||||||
|
buf
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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::*;
|
||||||
@@ -1,900 +0,0 @@
|
|||||||
//! The test driver handles the compilation and execution of the test cases.
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
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 _, bail};
|
|
||||||
use futures::{TryStreamExt, future::try_join_all};
|
|
||||||
use indexmap::IndexMap;
|
|
||||||
use revive_dt_common::types::{PlatformIdentifier, PrivateKeyAllocator};
|
|
||||||
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::metadata::{ContractIdent, ContractInstance, ContractPathAndIdent};
|
|
||||||
use revive_dt_format::steps::{
|
|
||||||
BalanceAssertionStep, Calldata, EtherValue, Expected, ExpectedOutput, FunctionCallStep, Method,
|
|
||||||
StepIdx, StepPath, StorageEmptyAssertionStep,
|
|
||||||
};
|
|
||||||
use revive_dt_format::{metadata::Metadata, steps::Step};
|
|
||||||
use revive_dt_node_interaction::EthereumNode;
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
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,
|
|
||||||
|
|
||||||
/// The private key allocator used for this case state. This is an Arc Mutex to allow for the
|
|
||||||
/// state to be cloned and for all of the clones to refer to the same allocator.
|
|
||||||
private_key_allocator: Arc<Mutex<PrivateKeyAllocator>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
private_key_allocator: Arc<Mutex<PrivateKeyAllocator>>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
compiled_contracts,
|
|
||||||
deployed_contracts,
|
|
||||||
variables: Default::default(),
|
|
||||||
compiler_version,
|
|
||||||
execution_reporter,
|
|
||||||
private_key_allocator,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn handle_step(
|
|
||||||
&mut self,
|
|
||||||
metadata: &Metadata,
|
|
||||||
step: &Step,
|
|
||||||
step_path: &StepPath,
|
|
||||||
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,
|
|
||||||
step_path,
|
|
||||||
node,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.context("Failed to handle the repetition step")?;
|
|
||||||
Ok(StepOutput::Repetition)
|
|
||||||
}
|
|
||||||
Step::AllocateAccount(account_allocation) => {
|
|
||||||
self.handle_account_allocation(account_allocation.variable_name.as_str())
|
|
||||||
.await
|
|
||||||
.context("Failed to allocate account")?;
|
|
||||||
Ok(StepOutput::AccountAllocation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.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],
|
|
||||||
step_path: &StepPath,
|
|
||||||
node: &dyn EthereumNode,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let tasks = (0..repetitions).map(|_| {
|
|
||||||
let mut state = self.clone();
|
|
||||||
async move {
|
|
||||||
for (step_idx, step) in steps.iter().enumerate() {
|
|
||||||
let step_path = step_path.append(step_idx);
|
|
||||||
state.handle_step(metadata, step, &step_path, node).await?;
|
|
||||||
}
|
|
||||||
Ok::<(), anyhow::Error>(())
|
|
||||||
}
|
|
||||||
});
|
|
||||||
try_join_all(tasks).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(level = "info", name = "Handling Account Allocation", skip_all)]
|
|
||||||
pub async fn handle_account_allocation(&mut self, variable_name: &str) -> anyhow::Result<()> {
|
|
||||||
let Some(variable_name) = variable_name.strip_prefix("$VARIABLE:") else {
|
|
||||||
bail!("Account allocation must start with $VARIABLE:");
|
|
||||||
};
|
|
||||||
|
|
||||||
let private_key = self.private_key_allocator.lock().await.allocate()?;
|
|
||||||
let account = private_key.address();
|
|
||||||
let variable = U256::from_be_slice(account.0.as_slice());
|
|
||||||
|
|
||||||
self.variables.insert(variable_name.to_string(), variable);
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
let caller = {
|
|
||||||
let context = self.default_resolution_context();
|
|
||||||
let resolver = node.resolver().await?;
|
|
||||||
input
|
|
||||||
.caller
|
|
||||||
.resolve_address(resolver.as_ref(), context)
|
|
||||||
.await?
|
|
||||||
};
|
|
||||||
if let (_, _, Some(receipt)) = self
|
|
||||||
.get_or_deploy_contract_instance(&instance, metadata, 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 = expected_address
|
|
||||||
.resolve_address(resolver, resolution_context)
|
|
||||||
.await?;
|
|
||||||
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(address) = balance_assertion.address.as_resolvable_address() else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let Some(instance) = address.strip_suffix(".address").map(ContractInstance::new) else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
self.get_or_deploy_contract_instance(
|
|
||||||
&instance,
|
|
||||||
metadata,
|
|
||||||
FunctionCallStep::default_caller_address(),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
node,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(level = "info", skip_all)]
|
|
||||||
pub async fn handle_balance_assertion_execution(
|
|
||||||
&mut self,
|
|
||||||
BalanceAssertionStep {
|
|
||||||
address,
|
|
||||||
expected_balance: amount,
|
|
||||||
..
|
|
||||||
}: &BalanceAssertionStep,
|
|
||||||
node: &dyn EthereumNode,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let resolver = node.resolver().await?;
|
|
||||||
let address = address
|
|
||||||
.resolve_address(resolver.as_ref(), self.default_resolution_context())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
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,
|
|
||||||
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(address) = storage_empty_assertion.address.as_resolvable_address() else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let Some(instance) = address.strip_suffix(".address").map(ContractInstance::new) else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
self.get_or_deploy_contract_instance(
|
|
||||||
&instance,
|
|
||||||
metadata,
|
|
||||||
FunctionCallStep::default_caller_address(),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
node,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(level = "info", skip_all)]
|
|
||||||
pub async fn handle_storage_empty_assertion_execution(
|
|
||||||
&mut self,
|
|
||||||
StorageEmptyAssertionStep {
|
|
||||||
address,
|
|
||||||
is_storage_empty,
|
|
||||||
..
|
|
||||||
}: &StorageEmptyAssertionStep,
|
|
||||||
node: &dyn EthereumNode,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let resolver = node.resolver().await?;
|
|
||||||
let address = address
|
|
||||||
.resolve_address(resolver.as_ref(), self.default_resolution_context())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
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,
|
|
||||||
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))
|
|
||||||
{
|
|
||||||
let metadata = self.metadata;
|
|
||||||
let step_futures =
|
|
||||||
self.platform_state
|
|
||||||
.iter_mut()
|
|
||||||
.map(|(node, platform_id, case_state)| {
|
|
||||||
let platform_id = *platform_id;
|
|
||||||
let node_ref = *node;
|
|
||||||
let step = step.clone();
|
|
||||||
let span = info_span!(
|
|
||||||
"Handling Step",
|
|
||||||
%step_idx,
|
|
||||||
platform = %platform_id,
|
|
||||||
);
|
|
||||||
async move {
|
|
||||||
let step_path = StepPath::from_iterator([step_idx]);
|
|
||||||
case_state
|
|
||||||
.handle_step(metadata, &step, &step_path, node_ref)
|
|
||||||
.await
|
|
||||||
.map_err(|e| (platform_id, e))
|
|
||||||
}
|
|
||||||
.instrument(span)
|
|
||||||
});
|
|
||||||
|
|
||||||
match try_join_all(step_futures).await {
|
|
||||||
Ok(_outputs) => {
|
|
||||||
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,
|
|
||||||
AccountAllocation,
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,7 @@ use std::{
|
|||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::{Arc, LazyLock},
|
||||||
};
|
};
|
||||||
|
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
@@ -19,7 +19,7 @@ use anyhow::{Context as _, Error, Result};
|
|||||||
use revive_dt_report::ExecutionSpecificReporter;
|
use revive_dt_report::ExecutionSpecificReporter;
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::sync::{Mutex, RwLock};
|
use tokio::sync::{Mutex, RwLock, Semaphore};
|
||||||
use tracing::{Instrument, debug, debug_span, instrument};
|
use tracing::{Instrument, debug, debug_span, instrument};
|
||||||
|
|
||||||
pub struct CachedCompiler<'a> {
|
pub struct CachedCompiler<'a> {
|
||||||
@@ -165,10 +165,22 @@ impl<'a> CachedCompiler<'a> {
|
|||||||
cache_value.compiler_output
|
cache_value.compiler_output
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
compilation_callback()
|
let compiler_output = compilation_callback()
|
||||||
.await
|
.await
|
||||||
.context("Compilation callback failed (cache miss path)")?
|
.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,
|
compiler: &dyn SolidityCompiler,
|
||||||
reporter: &ExecutionSpecificReporter,
|
reporter: &ExecutionSpecificReporter,
|
||||||
) -> Result<CompilerOutput> {
|
) -> 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())
|
let all_sources_in_dir = FilesWithExtensionIterator::new(metadata_directory.as_ref())
|
||||||
.with_allowed_extension("sol")
|
.with_allowed_extension("sol")
|
||||||
.with_use_cached_fs(true)
|
.with_use_cached_fs(true)
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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 {
|
impl NodePool {
|
||||||
/// Create a new Pool. This will start as many nodes as there are workers in `config`.
|
/// 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 concurrency_configuration = AsRef::<ConcurrencyConfiguration>::as_ref(&context);
|
||||||
let nodes = concurrency_configuration.number_of_nodes;
|
let nodes = concurrency_configuration.number_of_nodes;
|
||||||
|
|
||||||
@@ -33,11 +33,18 @@ impl NodePool {
|
|||||||
.join()
|
.join()
|
||||||
.map_err(|error| anyhow::anyhow!("failed to spawn node: {:?}", error))
|
.map_err(|error| anyhow::anyhow!("failed to spawn node: {:?}", error))
|
||||||
.context("Failed to join node spawn thread")?
|
.context("Failed to join node spawn thread")?
|
||||||
.map_err(|error| anyhow::anyhow!("node failed to spawn: {error}"))
|
|
||||||
.context("Node failed to spawn")?,
|
.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 {
|
Ok(Self {
|
||||||
nodes,
|
nodes,
|
||||||
next: Default::default(),
|
next: Default::default(),
|
||||||
@@ -0,0 +1,359 @@
|
|||||||
|
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::{ParsedMode, PlatformIdentifier};
|
||||||
|
use revive_dt_config::Context;
|
||||||
|
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, Report, Reporter, TestCaseStatus};
|
||||||
|
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)>,
|
||||||
|
only_execute_failed_tests: Option<&Report>,
|
||||||
|
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(only_execute_failed_tests) {
|
||||||
|
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,
|
||||||
|
only_execute_failed_tests: Option<&Report>,
|
||||||
|
) -> TestCheckFunctionResult {
|
||||||
|
self.check_metadata_file_ignored()?;
|
||||||
|
self.check_case_file_ignored()?;
|
||||||
|
self.check_target_compatibility()?;
|
||||||
|
self.check_evm_version_compatibility()?;
|
||||||
|
self.check_compiler_compatibility()?;
|
||||||
|
self.check_ignore_succeeded(only_execute_failed_tests)?;
|
||||||
|
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,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if the test case should be executed or not based on the passed report and whether the
|
||||||
|
/// user has instructed the tool to ignore the already succeeding test cases.
|
||||||
|
fn check_ignore_succeeded(
|
||||||
|
&self,
|
||||||
|
only_execute_failed_tests: Option<&Report>,
|
||||||
|
) -> TestCheckFunctionResult {
|
||||||
|
let Some(report) = only_execute_failed_tests else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let test_case_status = report
|
||||||
|
.test_case_information
|
||||||
|
.get(&(self.metadata_file_path.to_path_buf().into()))
|
||||||
|
.and_then(|obj| obj.get(&self.mode))
|
||||||
|
.and_then(|obj| obj.get(&self.case_idx))
|
||||||
|
.and_then(|obj| obj.status.as_ref());
|
||||||
|
|
||||||
|
match test_case_status {
|
||||||
|
Some(TestCaseStatus::Failed { .. }) => Ok(()),
|
||||||
|
Some(TestCaseStatus::Ignored { .. }) => Err((
|
||||||
|
"Ignored since it was ignored in a previous run",
|
||||||
|
indexmap! {},
|
||||||
|
)),
|
||||||
|
Some(TestCaseStatus::Succeeded { .. }) => {
|
||||||
|
Err(("Ignored since it succeeded in a prior run", indexmap! {}))
|
||||||
|
}
|
||||||
|
None => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
@@ -13,12 +13,14 @@ use anyhow::Context as _;
|
|||||||
use revive_dt_common::types::*;
|
use revive_dt_common::types::*;
|
||||||
use revive_dt_compiler::{SolidityCompiler, revive_resolc::Resolc, solc::Solc};
|
use revive_dt_compiler::{SolidityCompiler, revive_resolc::Resolc, solc::Solc};
|
||||||
use revive_dt_config::*;
|
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 revive_dt_node_interaction::EthereumNode;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
pub mod driver;
|
|
||||||
|
|
||||||
/// A trait that describes the interface for the platforms that are supported by the tool.
|
/// A trait that describes the interface for the platforms that are supported by the tool.
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
pub trait Platform {
|
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)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
|
||||||
pub struct KitchensinkPolkavmResolcPlatform;
|
pub struct KitchensinkPolkavmResolcPlatform;
|
||||||
|
|
||||||
@@ -137,6 +184,7 @@ impl Platform for KitchensinkPolkavmResolcPlatform {
|
|||||||
let node = SubstrateNode::new(
|
let node = SubstrateNode::new(
|
||||||
kitchensink_path,
|
kitchensink_path,
|
||||||
SubstrateNode::KITCHENSINK_EXPORT_CHAINSPEC_COMMAND,
|
SubstrateNode::KITCHENSINK_EXPORT_CHAINSPEC_COMMAND,
|
||||||
|
None,
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
let node = spawn_node(node, genesis)?;
|
let node = spawn_node(node, genesis)?;
|
||||||
@@ -189,6 +237,7 @@ impl Platform for KitchensinkRevmSolcPlatform {
|
|||||||
let node = SubstrateNode::new(
|
let node = SubstrateNode::new(
|
||||||
kitchensink_path,
|
kitchensink_path,
|
||||||
SubstrateNode::KITCHENSINK_EXPORT_CHAINSPEC_COMMAND,
|
SubstrateNode::KITCHENSINK_EXPORT_CHAINSPEC_COMMAND,
|
||||||
|
None,
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
let node = spawn_node(node, genesis)?;
|
let node = spawn_node(node, genesis)?;
|
||||||
@@ -233,14 +282,17 @@ impl Platform for ReviveDevNodePolkavmResolcPlatform {
|
|||||||
context: Context,
|
context: Context,
|
||||||
) -> anyhow::Result<JoinHandle<anyhow::Result<Box<dyn EthereumNode + Send + Sync>>>> {
|
) -> anyhow::Result<JoinHandle<anyhow::Result<Box<dyn EthereumNode + Send + Sync>>>> {
|
||||||
let genesis_configuration = AsRef::<GenesisConfiguration>::as_ref(&context);
|
let genesis_configuration = AsRef::<GenesisConfiguration>::as_ref(&context);
|
||||||
let revive_dev_node_path = AsRef::<ReviveDevNodeConfiguration>::as_ref(&context)
|
let revive_dev_node_configuration = AsRef::<ReviveDevNodeConfiguration>::as_ref(&context);
|
||||||
.path
|
|
||||||
.clone();
|
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();
|
let genesis = genesis_configuration.genesis()?.clone();
|
||||||
Ok(thread::spawn(move || {
|
Ok(thread::spawn(move || {
|
||||||
let node = SubstrateNode::new(
|
let node = SubstrateNode::new(
|
||||||
revive_dev_node_path,
|
revive_dev_node_path,
|
||||||
SubstrateNode::REVIVE_DEV_NODE_EXPORT_CHAINSPEC_COMMAND,
|
SubstrateNode::REVIVE_DEV_NODE_EXPORT_CHAINSPEC_COMMAND,
|
||||||
|
Some(revive_dev_node_consensus),
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
let node = spawn_node(node, genesis)?;
|
let node = spawn_node(node, genesis)?;
|
||||||
@@ -285,14 +337,17 @@ impl Platform for ReviveDevNodeRevmSolcPlatform {
|
|||||||
context: Context,
|
context: Context,
|
||||||
) -> anyhow::Result<JoinHandle<anyhow::Result<Box<dyn EthereumNode + Send + Sync>>>> {
|
) -> anyhow::Result<JoinHandle<anyhow::Result<Box<dyn EthereumNode + Send + Sync>>>> {
|
||||||
let genesis_configuration = AsRef::<GenesisConfiguration>::as_ref(&context);
|
let genesis_configuration = AsRef::<GenesisConfiguration>::as_ref(&context);
|
||||||
let revive_dev_node_path = AsRef::<ReviveDevNodeConfiguration>::as_ref(&context)
|
let revive_dev_node_configuration = AsRef::<ReviveDevNodeConfiguration>::as_ref(&context);
|
||||||
.path
|
|
||||||
.clone();
|
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();
|
let genesis = genesis_configuration.genesis()?.clone();
|
||||||
Ok(thread::spawn(move || {
|
Ok(thread::spawn(move || {
|
||||||
let node = SubstrateNode::new(
|
let node = SubstrateNode::new(
|
||||||
revive_dev_node_path,
|
revive_dev_node_path,
|
||||||
SubstrateNode::REVIVE_DEV_NODE_EXPORT_CHAINSPEC_COMMAND,
|
SubstrateNode::REVIVE_DEV_NODE_EXPORT_CHAINSPEC_COMMAND,
|
||||||
|
Some(revive_dev_node_consensus),
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
let node = spawn_node(node, genesis)?;
|
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> {
|
impl From<PlatformIdentifier> for Box<dyn Platform> {
|
||||||
fn from(value: PlatformIdentifier) -> Self {
|
fn from(value: PlatformIdentifier) -> Self {
|
||||||
match value {
|
match value {
|
||||||
PlatformIdentifier::GethEvmSolc => Box::new(GethEvmSolcPlatform) as Box<_>,
|
PlatformIdentifier::GethEvmSolc => Box::new(GethEvmSolcPlatform) as Box<_>,
|
||||||
|
PlatformIdentifier::LighthouseGethEvmSolc => {
|
||||||
|
Box::new(LighthouseGethEvmSolcPlatform) as Box<_>
|
||||||
|
}
|
||||||
PlatformIdentifier::KitchensinkPolkavmResolc => {
|
PlatformIdentifier::KitchensinkPolkavmResolc => {
|
||||||
Box::new(KitchensinkPolkavmResolcPlatform) as Box<_>
|
Box::new(KitchensinkPolkavmResolcPlatform) as Box<_>
|
||||||
}
|
}
|
||||||
@@ -328,6 +482,10 @@ impl From<PlatformIdentifier> for Box<dyn Platform> {
|
|||||||
PlatformIdentifier::ReviveDevNodeRevmSolc => {
|
PlatformIdentifier::ReviveDevNodeRevmSolc => {
|
||||||
Box::new(ReviveDevNodeRevmSolcPlatform) as Box<_>
|
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 {
|
fn from(value: PlatformIdentifier) -> Self {
|
||||||
match value {
|
match value {
|
||||||
PlatformIdentifier::GethEvmSolc => &GethEvmSolcPlatform as &dyn Platform,
|
PlatformIdentifier::GethEvmSolc => &GethEvmSolcPlatform as &dyn Platform,
|
||||||
|
PlatformIdentifier::LighthouseGethEvmSolc => {
|
||||||
|
&LighthouseGethEvmSolcPlatform as &dyn Platform
|
||||||
|
}
|
||||||
PlatformIdentifier::KitchensinkPolkavmResolc => {
|
PlatformIdentifier::KitchensinkPolkavmResolc => {
|
||||||
&KitchensinkPolkavmResolcPlatform as &dyn Platform
|
&KitchensinkPolkavmResolcPlatform as &dyn Platform
|
||||||
}
|
}
|
||||||
@@ -348,6 +509,10 @@ impl From<PlatformIdentifier> for &dyn Platform {
|
|||||||
PlatformIdentifier::ReviveDevNodeRevmSolc => {
|
PlatformIdentifier::ReviveDevNodeRevmSolc => {
|
||||||
&ReviveDevNodeRevmSolcPlatform as &dyn Platform
|
&ReviveDevNodeRevmSolcPlatform as &dyn Platform
|
||||||
}
|
}
|
||||||
|
PlatformIdentifier::ZombienetPolkavmResolc => {
|
||||||
|
&ZombienetPolkavmResolcPlatform as &dyn Platform
|
||||||
|
}
|
||||||
|
PlatformIdentifier::ZombienetRevmSolc => &ZombienetRevmSolcPlatform as &dyn Platform,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-744
@@ -1,55 +1,21 @@
|
|||||||
mod cached_compiler;
|
mod differential_benchmarks;
|
||||||
mod pool;
|
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 clap::Parser;
|
||||||
use futures::stream;
|
use revive_dt_report::ReportAggregator;
|
||||||
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 schemars::schema_for;
|
use schemars::schema_for;
|
||||||
use serde_json::{Value, json};
|
use tracing::info;
|
||||||
use tokio::sync::Mutex;
|
|
||||||
use tracing::{debug, error, info, info_span, instrument};
|
|
||||||
use tracing_subscriber::{EnvFilter, FmtSubscriber};
|
use tracing_subscriber::{EnvFilter, FmtSubscriber};
|
||||||
|
|
||||||
use revive_dt_common::{
|
use revive_dt_config::Context;
|
||||||
iterators::EitherIter,
|
use revive_dt_core::Platform;
|
||||||
types::{Mode, PrivateKeyAllocator},
|
use revive_dt_format::metadata::Metadata;
|
||||||
};
|
|
||||||
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,
|
|
||||||
metadata::{ContractPathAndIdent, Metadata, MetadataFile},
|
|
||||||
mode::ParsedMode,
|
|
||||||
steps::{FunctionCallStep, Step},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::cached_compiler::CachedCompiler;
|
use crate::{
|
||||||
use crate::pool::NodePool;
|
differential_benchmarks::handle_differential_benchmarks,
|
||||||
|
differential_tests::handle_differential_tests,
|
||||||
|
};
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
let (writer, _guard) = tracing_appender::non_blocking::NonBlockingBuilder::default()
|
let (writer, _guard) = tracing_appender::non_blocking::NonBlockingBuilder::default()
|
||||||
@@ -75,37 +41,37 @@ fn main() -> anyhow::Result<()> {
|
|||||||
let (reporter, report_aggregator_task) = ReportAggregator::new(context.clone()).into_task();
|
let (reporter, report_aggregator_task) = ReportAggregator::new(context.clone()).into_task();
|
||||||
|
|
||||||
match context {
|
match context {
|
||||||
Context::ExecuteTests(context) => {
|
Context::Test(context) => tokio::runtime::Builder::new_multi_thread()
|
||||||
let tests = collect_corpora(&context)
|
.worker_threads(context.concurrency_configuration.number_of_threads)
|
||||||
.context("Failed to collect corpus files from provided arguments")?
|
.enable_all()
|
||||||
.into_iter()
|
.build()
|
||||||
.inspect(|(corpus, _)| {
|
.expect("Failed building the Runtime")
|
||||||
reporter
|
.block_on(async move {
|
||||||
.report_corpus_file_discovery_event(corpus.clone())
|
let differential_tests_handling_task =
|
||||||
.expect("Can't fail")
|
handle_differential_tests(*context, reporter);
|
||||||
})
|
|
||||||
.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<_>>();
|
|
||||||
|
|
||||||
tokio::runtime::Builder::new_multi_thread()
|
futures::future::try_join(differential_tests_handling_task, report_aggregator_task)
|
||||||
.worker_threads(context.concurrency_configuration.number_of_threads)
|
.await?;
|
||||||
.enable_all()
|
|
||||||
.build()
|
Ok(())
|
||||||
.expect("Failed building the Runtime")
|
}),
|
||||||
.block_on(async move {
|
Context::Benchmark(context) => tokio::runtime::Builder::new_multi_thread()
|
||||||
execute_corpus(*context, &tests, reporter, report_aggregator_task)
|
.worker_threads(context.concurrency_configuration.number_of_threads)
|
||||||
.await
|
.enable_all()
|
||||||
.context("Failed to execute corpus")
|
.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 => {
|
Context::ExportJsonSchema => {
|
||||||
let schema = schema_for!(Metadata);
|
let schema = schema_for!(Metadata);
|
||||||
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
|
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
|
||||||
@@ -113,671 +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 private_key_allocator = Arc::new(Mutex::new(PrivateKeyAllocator::new(
|
|
||||||
context.wallet_configuration.highest_private_key_exclusive(),
|
|
||||||
)));
|
|
||||||
|
|
||||||
let reporter = test.reporter.clone();
|
|
||||||
let result =
|
|
||||||
handle_case_driver(&test, cached_compiler, private_key_allocator).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>>,
|
|
||||||
private_key_allocator: Arc<Mutex<PrivateKeyAllocator>>,
|
|
||||||
) -> 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) => 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 = 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();
|
|
||||||
let private_key_allocator = private_key_allocator.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(),
|
|
||||||
private_key_allocator,
|
|
||||||
);
|
|
||||||
|
|
||||||
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>)>;
|
|
||||||
|
|||||||
@@ -14,11 +14,8 @@ revive-dt-common = { workspace = true }
|
|||||||
revive-common = { workspace = true }
|
revive-common = { workspace = true }
|
||||||
|
|
||||||
alloy = { workspace = true }
|
alloy = { workspace = true }
|
||||||
alloy-primitives = { workspace = true }
|
|
||||||
alloy-sol-types = { workspace = true }
|
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
regex = { workspace = true }
|
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
schemars = { workspace = true }
|
schemars = { workspace = true }
|
||||||
semver = { workspace = true }
|
semver = { workspace = true }
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use revive_dt_common::{macros::define_wrapper_type, types::Mode};
|
use revive_dt_common::{
|
||||||
|
macros::define_wrapper_type,
|
||||||
use crate::{
|
types::{Mode, ParsedMode},
|
||||||
mode::ParsedMode,
|
|
||||||
steps::{Expected, RepeatStep, Step},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::steps::*;
|
||||||
|
|
||||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq, JsonSchema)]
|
#[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq, JsonSchema)]
|
||||||
pub struct Case {
|
pub struct Case {
|
||||||
/// An optional name of the test case.
|
/// An optional name of the test case.
|
||||||
|
|||||||
@@ -3,6 +3,5 @@
|
|||||||
pub mod case;
|
pub mod case;
|
||||||
pub mod corpus;
|
pub mod corpus;
|
||||||
pub mod metadata;
|
pub mod metadata;
|
||||||
pub mod mode;
|
|
||||||
pub mod steps;
|
pub mod steps;
|
||||||
pub mod traits;
|
pub mod traits;
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ use revive_dt_common::{
|
|||||||
cached_fs::read_to_string,
|
cached_fs::read_to_string,
|
||||||
iterators::FilesWithExtensionIterator,
|
iterators::FilesWithExtensionIterator,
|
||||||
macros::define_wrapper_type,
|
macros::define_wrapper_type,
|
||||||
types::{Mode, VmIdentifier},
|
types::{Mode, ParsedMode, VmIdentifier},
|
||||||
};
|
};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use crate::{case::Case, mode::ParsedMode};
|
use crate::case::Case;
|
||||||
|
|
||||||
pub const METADATA_FILE_EXTENSION: &str = "json";
|
pub const METADATA_FILE_EXTENSION: &str = "json";
|
||||||
pub const SOLIDITY_CASE_FILE_EXTENSION: &str = "sol";
|
pub const SOLIDITY_CASE_FILE_EXTENSION: &str = "sol";
|
||||||
|
|||||||
@@ -1,257 +0,0 @@
|
|||||||
use anyhow::Context as _;
|
|
||||||
use regex::Regex;
|
|
||||||
use revive_dt_common::iterators::EitherIter;
|
|
||||||
use revive_dt_common::types::{Mode, ModeOptimizerSetting, ModePipeline};
|
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::fmt::Display;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use std::sync::LazyLock;
|
|
||||||
|
|
||||||
/// This represents a mode that has been parsed from test metadata.
|
|
||||||
///
|
|
||||||
/// Mode strings can take the following form (in pseudo-regex):
|
|
||||||
///
|
|
||||||
/// ```text
|
|
||||||
/// [YEILV][+-]? (M[0123sz])? <semver>?
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// We can parse valid mode strings into [`ParsedMode`] using [`ParsedMode::from_str`].
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)]
|
|
||||||
#[serde(try_from = "String", into = "String")]
|
|
||||||
pub struct ParsedMode {
|
|
||||||
pub pipeline: Option<ModePipeline>,
|
|
||||||
pub optimize_flag: Option<bool>,
|
|
||||||
pub optimize_setting: Option<ModeOptimizerSetting>,
|
|
||||||
pub version: Option<semver::VersionReq>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for ParsedMode {
|
|
||||||
type Err = anyhow::Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
static REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
|
||||||
Regex::new(r"(?x)
|
|
||||||
^
|
|
||||||
(?:(?P<pipeline>[YEILV])(?P<optimize_flag>[+-])?)? # Pipeline to use eg Y, E+, E-
|
|
||||||
\s*
|
|
||||||
(?P<optimize_setting>M[a-zA-Z0-9])? # Optimize setting eg M0, Ms, Mz
|
|
||||||
\s*
|
|
||||||
(?P<version>[>=<]*\d+(?:\.\d+)*)? # Optional semver version eg >=0.8.0, 0.7, <0.8
|
|
||||||
$
|
|
||||||
").unwrap()
|
|
||||||
});
|
|
||||||
|
|
||||||
let Some(caps) = REGEX.captures(s) else {
|
|
||||||
anyhow::bail!("Cannot parse mode '{s}' from string");
|
|
||||||
};
|
|
||||||
|
|
||||||
let pipeline = match caps.name("pipeline") {
|
|
||||||
Some(m) => Some(
|
|
||||||
ModePipeline::from_str(m.as_str())
|
|
||||||
.context("Failed to parse mode pipeline from string")?,
|
|
||||||
),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let optimize_flag = caps.name("optimize_flag").map(|m| m.as_str() == "+");
|
|
||||||
|
|
||||||
let optimize_setting = match caps.name("optimize_setting") {
|
|
||||||
Some(m) => Some(
|
|
||||||
ModeOptimizerSetting::from_str(m.as_str())
|
|
||||||
.context("Failed to parse optimizer setting from string")?,
|
|
||||||
),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let version = match caps.name("version") {
|
|
||||||
Some(m) => Some(
|
|
||||||
semver::VersionReq::parse(m.as_str())
|
|
||||||
.map_err(|e| {
|
|
||||||
anyhow::anyhow!(
|
|
||||||
"Cannot parse the version requirement '{}': {e}",
|
|
||||||
m.as_str()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.context("Failed to parse semver requirement from mode string")?,
|
|
||||||
),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(ParsedMode {
|
|
||||||
pipeline,
|
|
||||||
optimize_flag,
|
|
||||||
optimize_setting,
|
|
||||||
version,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for ParsedMode {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
let mut has_written = false;
|
|
||||||
|
|
||||||
if let Some(pipeline) = self.pipeline {
|
|
||||||
pipeline.fmt(f)?;
|
|
||||||
if let Some(optimize_flag) = self.optimize_flag {
|
|
||||||
f.write_str(if optimize_flag { "+" } else { "-" })?;
|
|
||||||
}
|
|
||||||
has_written = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(optimize_setting) = self.optimize_setting {
|
|
||||||
if has_written {
|
|
||||||
f.write_str(" ")?;
|
|
||||||
}
|
|
||||||
optimize_setting.fmt(f)?;
|
|
||||||
has_written = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(version) = &self.version {
|
|
||||||
if has_written {
|
|
||||||
f.write_str(" ")?;
|
|
||||||
}
|
|
||||||
version.fmt(f)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ParsedMode> for String {
|
|
||||||
fn from(parsed_mode: ParsedMode) -> Self {
|
|
||||||
parsed_mode.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<String> for ParsedMode {
|
|
||||||
type Error = anyhow::Error;
|
|
||||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
|
||||||
ParsedMode::from_str(&value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ParsedMode {
|
|
||||||
/// This takes a [`ParsedMode`] and expands it into a list of [`Mode`]s that we should try.
|
|
||||||
pub fn to_modes(&self) -> impl Iterator<Item = Mode> {
|
|
||||||
let pipeline_iter = self.pipeline.as_ref().map_or_else(
|
|
||||||
|| EitherIter::A(ModePipeline::test_cases()),
|
|
||||||
|p| EitherIter::B(std::iter::once(*p)),
|
|
||||||
);
|
|
||||||
|
|
||||||
let optimize_flag_setting = self.optimize_flag.map(|flag| {
|
|
||||||
if flag {
|
|
||||||
ModeOptimizerSetting::M3
|
|
||||||
} else {
|
|
||||||
ModeOptimizerSetting::M0
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let optimize_flag_iter = match optimize_flag_setting {
|
|
||||||
Some(setting) => EitherIter::A(std::iter::once(setting)),
|
|
||||||
None => EitherIter::B(ModeOptimizerSetting::test_cases()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let optimize_settings_iter = self.optimize_setting.as_ref().map_or_else(
|
|
||||||
|| EitherIter::A(optimize_flag_iter),
|
|
||||||
|s| EitherIter::B(std::iter::once(*s)),
|
|
||||||
);
|
|
||||||
|
|
||||||
pipeline_iter.flat_map(move |pipeline| {
|
|
||||||
optimize_settings_iter
|
|
||||||
.clone()
|
|
||||||
.map(move |optimize_setting| Mode {
|
|
||||||
pipeline,
|
|
||||||
optimize_setting,
|
|
||||||
version: self.version.clone(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return a set of [`Mode`]s that correspond to the given [`ParsedMode`]s.
|
|
||||||
/// This avoids any duplicate entries.
|
|
||||||
pub fn many_to_modes<'a>(
|
|
||||||
parsed: impl Iterator<Item = &'a ParsedMode>,
|
|
||||||
) -> impl Iterator<Item = Mode> {
|
|
||||||
let modes: HashSet<_> = parsed.flat_map(|p| p.to_modes()).collect();
|
|
||||||
modes.into_iter()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parsed_mode_from_str() {
|
|
||||||
let strings = vec![
|
|
||||||
("Mz", "Mz"),
|
|
||||||
("Y", "Y"),
|
|
||||||
("Y+", "Y+"),
|
|
||||||
("Y-", "Y-"),
|
|
||||||
("E", "E"),
|
|
||||||
("E+", "E+"),
|
|
||||||
("E-", "E-"),
|
|
||||||
("Y M0", "Y M0"),
|
|
||||||
("Y M1", "Y M1"),
|
|
||||||
("Y M2", "Y M2"),
|
|
||||||
("Y M3", "Y M3"),
|
|
||||||
("Y Ms", "Y Ms"),
|
|
||||||
("Y Mz", "Y Mz"),
|
|
||||||
("E M0", "E M0"),
|
|
||||||
("E M1", "E M1"),
|
|
||||||
("E M2", "E M2"),
|
|
||||||
("E M3", "E M3"),
|
|
||||||
("E Ms", "E Ms"),
|
|
||||||
("E Mz", "E Mz"),
|
|
||||||
// When stringifying semver again, 0.8.0 becomes ^0.8.0 (same meaning)
|
|
||||||
("Y 0.8.0", "Y ^0.8.0"),
|
|
||||||
("E+ 0.8.0", "E+ ^0.8.0"),
|
|
||||||
("Y M3 >=0.8.0", "Y M3 >=0.8.0"),
|
|
||||||
("E Mz <0.7.0", "E Mz <0.7.0"),
|
|
||||||
// We can parse +- _and_ M1/M2 but the latter takes priority.
|
|
||||||
("Y+ M1 0.8.0", "Y+ M1 ^0.8.0"),
|
|
||||||
("E- M2 0.7.0", "E- M2 ^0.7.0"),
|
|
||||||
// We don't see this in the wild but it is parsed.
|
|
||||||
("<=0.8", "<=0.8"),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (actual, expected) in strings {
|
|
||||||
let parsed = ParsedMode::from_str(actual)
|
|
||||||
.unwrap_or_else(|_| panic!("Failed to parse mode string '{actual}'"));
|
|
||||||
assert_eq!(
|
|
||||||
expected,
|
|
||||||
parsed.to_string(),
|
|
||||||
"Mode string '{actual}' did not parse to '{expected}': got '{parsed}'"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parsed_mode_to_test_modes() {
|
|
||||||
let strings = vec![
|
|
||||||
("Mz", vec!["Y Mz", "E Mz"]),
|
|
||||||
("Y", vec!["Y M0", "Y M3"]),
|
|
||||||
("E", vec!["E M0", "E M3"]),
|
|
||||||
("Y+", vec!["Y M3"]),
|
|
||||||
("Y-", vec!["Y M0"]),
|
|
||||||
("Y <=0.8", vec!["Y M0 <=0.8", "Y M3 <=0.8"]),
|
|
||||||
(
|
|
||||||
"<=0.8",
|
|
||||||
vec!["Y M0 <=0.8", "Y M3 <=0.8", "E M0 <=0.8", "E M3 <=0.8"],
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (actual, expected) in strings {
|
|
||||||
let parsed = ParsedMode::from_str(actual)
|
|
||||||
.unwrap_or_else(|_| panic!("Failed to parse mode string '{actual}'"));
|
|
||||||
let expected_set: HashSet<_> = expected.into_iter().map(|s| s.to_owned()).collect();
|
|
||||||
let actual_set: HashSet<_> = parsed.to_modes().map(|m| m.to_string()).collect();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
expected_set, actual_set,
|
|
||||||
"Mode string '{actual}' did not expand to '{expected_set:?}': got '{actual_set:?}'"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
use std::{collections::HashMap, fmt::Display, str::FromStr};
|
use std::{collections::HashMap, fmt::Display, str::FromStr};
|
||||||
|
|
||||||
|
use alloy::primitives::{FixedBytes, utils::parse_units};
|
||||||
use alloy::{
|
use alloy::{
|
||||||
eips::BlockNumberOrTag,
|
eips::BlockNumberOrTag,
|
||||||
json_abi::Function,
|
json_abi::Function,
|
||||||
@@ -7,7 +8,6 @@ use alloy::{
|
|||||||
primitives::{Address, Bytes, U256},
|
primitives::{Address, Bytes, U256},
|
||||||
rpc::types::TransactionRequest,
|
rpc::types::TransactionRequest,
|
||||||
};
|
};
|
||||||
use alloy_primitives::{FixedBytes, utils::parse_units};
|
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, stream};
|
use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, stream};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
@@ -537,7 +537,7 @@ impl FunctionCallStep {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parse this input into a legacy transaction.
|
/// Parse this input into a legacy transaction.
|
||||||
pub async fn legacy_transaction(
|
pub async fn as_transaction(
|
||||||
&self,
|
&self,
|
||||||
resolver: &(impl ResolverApi + ?Sized),
|
resolver: &(impl ResolverApi + ?Sized),
|
||||||
context: ResolutionContext<'_>,
|
context: ResolutionContext<'_>,
|
||||||
@@ -959,9 +959,9 @@ impl<'de> Deserialize<'de> for EtherValue {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
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::{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 std::{collections::HashMap, pin::Pin};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -1115,7 +1115,7 @@ mod tests {
|
|||||||
let encoded = input.encoded_input(&resolver, context).await.unwrap();
|
let encoded = input.encoded_input(&resolver, context).await.unwrap();
|
||||||
assert!(encoded.0.starts_with(&selector));
|
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();
|
let decoded: T = T::abi_decode(&encoded.0[4..]).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
decoded.0,
|
decoded.0,
|
||||||
@@ -1162,7 +1162,7 @@ mod tests {
|
|||||||
let encoded = input.encoded_input(&resolver, context).await.unwrap();
|
let encoded = input.encoded_input(&resolver, context).await.unwrap();
|
||||||
assert!(encoded.0.starts_with(&selector));
|
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();
|
let decoded: T = T::abi_decode(&encoded.0[4..]).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
decoded.0,
|
decoded.0,
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ use std::pin::Pin;
|
|||||||
|
|
||||||
use alloy::eips::BlockNumberOrTag;
|
use alloy::eips::BlockNumberOrTag;
|
||||||
use alloy::json_abi::JsonAbi;
|
use alloy::json_abi::JsonAbi;
|
||||||
|
use alloy::primitives::TxHash;
|
||||||
use alloy::primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, ChainId, U256};
|
use alloy::primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, ChainId, U256};
|
||||||
use alloy_primitives::TxHash;
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use crate::metadata::{ContractIdent, ContractInstance};
|
use crate::metadata::{ContractIdent, ContractInstance};
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ revive-dt-format = { workspace = true }
|
|||||||
|
|
||||||
alloy = { workspace = true }
|
alloy = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
futures = { workspace = true }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
@@ -3,22 +3,38 @@
|
|||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use alloy::primitives::{Address, StorageKey, TxHash, U256};
|
use alloy::network::Ethereum;
|
||||||
|
use alloy::primitives::{Address, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256};
|
||||||
|
use alloy::providers::DynProvider;
|
||||||
use alloy::rpc::types::trace::geth::{DiffMode, GethDebugTracingOptions, GethTrace};
|
use alloy::rpc::types::trace::geth::{DiffMode, GethDebugTracingOptions, GethTrace};
|
||||||
use alloy::rpc::types::{EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest};
|
use alloy::rpc::types::{EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use futures::Stream;
|
||||||
use revive_common::EVMVersion;
|
use revive_common::EVMVersion;
|
||||||
use revive_dt_format::traits::ResolverApi;
|
use revive_dt_format::traits::ResolverApi;
|
||||||
|
|
||||||
/// An interface for all interactions with Ethereum compatible nodes.
|
/// An interface for all interactions with Ethereum compatible nodes.
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
pub trait EthereumNode {
|
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;
|
fn id(&self) -> usize;
|
||||||
|
|
||||||
/// Returns the nodes connection string.
|
/// Returns the nodes connection string.
|
||||||
fn connection_string(&self) -> &str;
|
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].
|
/// Execute the [TransactionRequest] and return a [TransactionReceipt].
|
||||||
fn execute_transaction(
|
fn execute_transaction(
|
||||||
&self,
|
&self,
|
||||||
@@ -50,4 +66,47 @@ pub trait EthereumNode {
|
|||||||
|
|
||||||
/// Returns the EVM version of the node.
|
/// Returns the EVM version of the node.
|
||||||
fn evm_version(&self) -> EVMVersion;
|
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>>>>>
|
||||||
|
+ '_,
|
||||||
|
>,
|
||||||
|
>;
|
||||||
|
|
||||||
|
fn provider(&self)
|
||||||
|
-> Pin<Box<dyn Future<Output = anyhow::Result<DynProvider<Ethereum>>> + '_>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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>,
|
||||||
|
|
||||||
|
/// The ref time for substrate based chains.
|
||||||
|
pub ref_time: u128,
|
||||||
|
|
||||||
|
/// The max ref time for substrate based chains.
|
||||||
|
pub max_ref_time: u64,
|
||||||
|
|
||||||
|
/// The proof size for substrate based chains.
|
||||||
|
pub proof_size: u128,
|
||||||
|
|
||||||
|
/// The max proof size for substrate based chains.
|
||||||
|
pub max_proof_size: u64,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ rust-version.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
alloy = { workspace = true }
|
alloy = { workspace = true }
|
||||||
|
futures = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
|
tower = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
|
|
||||||
revive-common = { workspace = true }
|
revive-common = { workspace = true }
|
||||||
@@ -22,9 +24,13 @@ revive-dt-node-interaction = { workspace = true }
|
|||||||
|
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
serde_with = { workspace = true }
|
||||||
|
serde_yaml_ng = { workspace = true }
|
||||||
|
|
||||||
sp-core = { workspace = true }
|
sp-core = { workspace = true }
|
||||||
sp-runtime = { workspace = true }
|
sp-runtime = { workspace = true }
|
||||||
|
subxt = { workspace = true }
|
||||||
|
zombienet-sdk = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
temp-dir = { workspace = true }
|
temp-dir = { workspace = true }
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
|
use alloy::primitives::ChainId;
|
||||||
|
|
||||||
/// This constant defines how much Wei accounts are pre-seeded with in genesis.
|
/// 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
|
/// 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.
|
/// some issues with different values of the initial balance on substrate.
|
||||||
pub const INITIAL_BALANCE: u128 = 10u128.pow(37);
|
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;
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
mod process;
|
||||||
|
|
||||||
|
pub use process::*;
|
||||||
@@ -68,7 +68,7 @@ impl Process {
|
|||||||
command_building_callback(&mut command, stdout_logs_file, stderr_logs_file);
|
command_building_callback(&mut command, stdout_logs_file, stderr_logs_file);
|
||||||
command
|
command
|
||||||
};
|
};
|
||||||
let child = command
|
let mut child = command
|
||||||
.spawn()
|
.spawn()
|
||||||
.context("Failed to spawn the built command")?;
|
.context("Failed to spawn the built command")?;
|
||||||
|
|
||||||
@@ -93,23 +93,49 @@ impl Process {
|
|||||||
let mut stdout_lines = BufReader::new(stdout_logs_file).lines();
|
let mut stdout_lines = BufReader::new(stdout_logs_file).lines();
|
||||||
let mut stderr_lines = BufReader::new(stderr_logs_file).lines();
|
let mut stderr_lines = BufReader::new(stderr_logs_file).lines();
|
||||||
|
|
||||||
|
let mut stdout = String::new();
|
||||||
|
let mut stderr = String::new();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let stdout_line = stdout_lines.next().and_then(Result::ok);
|
let stdout_line = stdout_lines.next().and_then(Result::ok);
|
||||||
let stderr_line = stderr_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 =
|
let check_result =
|
||||||
check_function(stdout_line.as_deref(), stderr_line.as_deref())
|
check_function(stdout_line.as_deref(), stderr_line.as_deref()).context(
|
||||||
.context("Failed to wait for the process to be ready")?;
|
format!(
|
||||||
|
"Failed to wait for the process to be ready - {stdout} - {stderr}"
|
||||||
|
),
|
||||||
|
)?;
|
||||||
|
|
||||||
if check_result {
|
if check_result {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if Instant::now().duration_since(spawn_time) > max_wait_duration {
|
if Instant::now().duration_since(spawn_time) > max_wait_duration {
|
||||||
bail!("Waited for the process to start but it failed to start in time")
|
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 {
|
Ok(Self {
|
||||||
@@ -137,6 +163,9 @@ pub enum ProcessReadinessWaitBehavior {
|
|||||||
/// straight away.
|
/// straight away.
|
||||||
NoStartupWait,
|
NoStartupWait,
|
||||||
|
|
||||||
|
/// Waits for the command to exit.
|
||||||
|
WaitForCommandToExit,
|
||||||
|
|
||||||
/// The process does require some amount of wait duration after it's been started.
|
/// The process does require some amount of wait duration after it's been started.
|
||||||
WaitDuration(Duration),
|
WaitDuration(Duration),
|
||||||
|
|
||||||
@@ -3,11 +3,10 @@
|
|||||||
use alloy::genesis::Genesis;
|
use alloy::genesis::Genesis;
|
||||||
use revive_dt_node_interaction::EthereumNode;
|
use revive_dt_node_interaction::EthereumNode;
|
||||||
|
|
||||||
pub mod common;
|
|
||||||
pub mod constants;
|
pub mod constants;
|
||||||
pub mod geth;
|
pub mod helpers;
|
||||||
pub mod process;
|
pub mod node_implementations;
|
||||||
pub mod substrate;
|
pub mod provider_utils;
|
||||||
|
|
||||||
/// An abstract interface for testing nodes.
|
/// An abstract interface for testing nodes.
|
||||||
pub trait Node: EthereumNode {
|
pub trait Node: EthereumNode {
|
||||||
|
|||||||
@@ -20,18 +20,22 @@ use alloy::{
|
|||||||
network::{Ethereum, EthereumWallet, NetworkWallet},
|
network::{Ethereum, EthereumWallet, NetworkWallet},
|
||||||
primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256},
|
primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256},
|
||||||
providers::{
|
providers::{
|
||||||
Provider, ProviderBuilder,
|
Provider,
|
||||||
ext::DebugApi,
|
ext::DebugApi,
|
||||||
fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller},
|
fillers::{CachedNonceManager, ChainIdFiller, NonceFiller},
|
||||||
},
|
},
|
||||||
rpc::types::{
|
rpc::types::{
|
||||||
EIP1186AccountProofResponse, TransactionRequest,
|
EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest,
|
||||||
trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame},
|
trace::geth::{
|
||||||
|
DiffMode, GethDebugTracingOptions, GethTrace, PreStateConfig, PreStateFrame,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
|
use futures::{FutureExt, Stream, StreamExt};
|
||||||
use revive_common::EVMVersion;
|
use revive_common::EVMVersion;
|
||||||
use tracing::{Instrument, instrument};
|
use tokio::sync::OnceCell;
|
||||||
|
use tracing::{Instrument, error, instrument};
|
||||||
|
|
||||||
use revive_dt_common::{
|
use revive_dt_common::{
|
||||||
fs::clear_directory,
|
fs::clear_directory,
|
||||||
@@ -39,13 +43,13 @@ use revive_dt_common::{
|
|||||||
};
|
};
|
||||||
use revive_dt_config::*;
|
use revive_dt_config::*;
|
||||||
use revive_dt_format::traits::ResolverApi;
|
use revive_dt_format::traits::ResolverApi;
|
||||||
use revive_dt_node_interaction::EthereumNode;
|
use revive_dt_node_interaction::{EthereumNode, MinedBlockInformation};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Node,
|
Node,
|
||||||
common::FallbackGasFiller,
|
constants::{CHAIN_ID, INITIAL_BALANCE},
|
||||||
constants::INITIAL_BALANCE,
|
helpers::{Process, ProcessReadinessWaitBehavior},
|
||||||
process::{Process, ProcessReadinessWaitBehavior},
|
provider_utils::{ConcreteProvider, FallbackGasFiller, construct_concurrency_limited_provider},
|
||||||
};
|
};
|
||||||
|
|
||||||
static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
|
static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
|
||||||
@@ -70,7 +74,7 @@ pub struct GethNode {
|
|||||||
start_timeout: Duration,
|
start_timeout: Duration,
|
||||||
wallet: Arc<EthereumWallet>,
|
wallet: Arc<EthereumWallet>,
|
||||||
nonce_manager: CachedNonceManager,
|
nonce_manager: CachedNonceManager,
|
||||||
chain_id_filler: ChainIdFiller,
|
provider: OnceCell<ConcreteProvider<Ethereum, Arc<EthereumWallet>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GethNode {
|
impl GethNode {
|
||||||
@@ -119,8 +123,8 @@ impl GethNode {
|
|||||||
handle: None,
|
handle: None,
|
||||||
start_timeout: geth_configuration.start_timeout_ms,
|
start_timeout: geth_configuration.start_timeout_ms,
|
||||||
wallet: wallet.clone(),
|
wallet: wallet.clone(),
|
||||||
chain_id_filler: Default::default(),
|
|
||||||
nonce_manager: Default::default(),
|
nonce_manager: Default::default(),
|
||||||
|
provider: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,7 +239,7 @@ impl GethNode {
|
|||||||
match process {
|
match process {
|
||||||
Ok(process) => self.handle = Some(process),
|
Ok(process) => self.handle = Some(process),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
tracing::error!(?err, "Failed to start geth, shutting down gracefully");
|
error!(?err, "Failed to start geth, shutting down gracefully");
|
||||||
self.shutdown()
|
self.shutdown()
|
||||||
.context("Failed to gracefully shutdown after geth start error")?;
|
.context("Failed to gracefully shutdown after geth start error")?;
|
||||||
return Err(err);
|
return Err(err);
|
||||||
@@ -245,27 +249,29 @@ impl GethNode {
|
|||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn provider(
|
async fn provider(&self) -> anyhow::Result<ConcreteProvider<Ethereum, Arc<EthereumWallet>>> {
|
||||||
&self,
|
self.provider
|
||||||
) -> anyhow::Result<FillProvider<impl TxFiller<Ethereum>, impl Provider<Ethereum>, Ethereum>>
|
.get_or_try_init(|| async move {
|
||||||
{
|
construct_concurrency_limited_provider::<Ethereum, _>(
|
||||||
ProviderBuilder::new()
|
self.connection_string.as_str(),
|
||||||
.disable_recommended_fillers()
|
FallbackGasFiller::default(),
|
||||||
.filler(FallbackGasFiller::new(
|
ChainIdFiller::new(Some(CHAIN_ID)),
|
||||||
25_000_000,
|
NonceFiller::new(self.nonce_manager.clone()),
|
||||||
1_000_000_000,
|
self.wallet.clone(),
|
||||||
1_000_000_000,
|
)
|
||||||
))
|
.await
|
||||||
.filler(self.chain_id_filler.clone())
|
.context("Failed to construct the provider")
|
||||||
.filler(NonceFiller::new(self.nonce_manager.clone()))
|
})
|
||||||
.wallet(self.wallet.clone())
|
|
||||||
.connect(&self.connection_string)
|
|
||||||
.await
|
.await
|
||||||
.map_err(Into::into)
|
.cloned()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EthereumNode for GethNode {
|
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 {
|
fn id(&self) -> usize {
|
||||||
self.id as _
|
self.id as _
|
||||||
}
|
}
|
||||||
@@ -274,6 +280,50 @@ impl EthereumNode for GethNode {
|
|||||||
&self.connection_string
|
&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(
|
#[instrument(
|
||||||
level = "info",
|
level = "info",
|
||||||
skip_all,
|
skip_all,
|
||||||
@@ -283,8 +333,7 @@ impl EthereumNode for GethNode {
|
|||||||
fn execute_transaction(
|
fn execute_transaction(
|
||||||
&self,
|
&self,
|
||||||
transaction: TransactionRequest,
|
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 {
|
Box::pin(async move {
|
||||||
let provider = self
|
let provider = self
|
||||||
.provider()
|
.provider()
|
||||||
@@ -292,12 +341,12 @@ impl EthereumNode for GethNode {
|
|||||||
.context("Failed to create provider for transaction submission")?;
|
.context("Failed to create provider for transaction submission")?;
|
||||||
|
|
||||||
let pending_transaction = provider
|
let pending_transaction = provider
|
||||||
.send_transaction(transaction)
|
.send_transaction(transaction)
|
||||||
.await
|
.await
|
||||||
.inspect_err(
|
.inspect_err(
|
||||||
|err| tracing::error!(%err, "Encountered an error when submitting the transaction"),
|
|err| error!(%err, "Encountered an error when submitting the transaction"),
|
||||||
)
|
)
|
||||||
.context("Failed to submit transaction to geth node")?;
|
.context("Failed to submit transaction to geth node")?;
|
||||||
let transaction_hash = *pending_transaction.tx_hash();
|
let transaction_hash = *pending_transaction.tx_hash();
|
||||||
|
|
||||||
// The following is a fix for the "transaction indexing is in progress" error that we used
|
// The following is a fix for the "transaction indexing is in progress" error that we used
|
||||||
@@ -317,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
|
// 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
|
// 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.
|
// backoff each time we attempt to get the receipt and find that it's not available.
|
||||||
let provider = Arc::new(provider);
|
|
||||||
poll(
|
poll(
|
||||||
Self::RECEIPT_POLLING_DURATION,
|
Self::RECEIPT_POLLING_DURATION,
|
||||||
PollingWaitBehavior::Constant(Duration::from_millis(200)),
|
PollingWaitBehavior::Constant(Duration::from_millis(200)),
|
||||||
@@ -351,14 +399,12 @@ impl EthereumNode for GethNode {
|
|||||||
&self,
|
&self,
|
||||||
tx_hash: TxHash,
|
tx_hash: TxHash,
|
||||||
trace_options: GethDebugTracingOptions,
|
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 {
|
Box::pin(async move {
|
||||||
let provider = Arc::new(
|
let provider = self
|
||||||
self.provider()
|
.provider()
|
||||||
.await
|
.await
|
||||||
.context("Failed to create provider for tracing")?,
|
.context("Failed to create provider for tracing")?;
|
||||||
);
|
|
||||||
poll(
|
poll(
|
||||||
Self::TRACE_POLLING_DURATION,
|
Self::TRACE_POLLING_DURATION,
|
||||||
PollingWaitBehavior::Constant(Duration::from_millis(200)),
|
PollingWaitBehavior::Constant(Duration::from_millis(200)),
|
||||||
@@ -456,14 +502,68 @@ impl EthereumNode for GethNode {
|
|||||||
fn evm_version(&self) -> EVMVersion {
|
fn evm_version(&self) -> EVMVersion {
|
||||||
EVMVersion::Cancun
|
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(),
|
||||||
|
ref_time: 0,
|
||||||
|
max_ref_time: 0,
|
||||||
|
proof_size: 0,
|
||||||
|
max_proof_size: 0,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Box::pin(mined_block_information_stream)
|
||||||
|
as Pin<Box<dyn Stream<Item = MinedBlockInformation>>>)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn provider(
|
||||||
|
&self,
|
||||||
|
) -> Pin<Box<dyn Future<Output = anyhow::Result<alloy::providers::DynProvider<Ethereum>>> + '_>>
|
||||||
|
{
|
||||||
|
Box::pin(
|
||||||
|
self.provider()
|
||||||
|
.map(|provider| provider.map(|provider| provider.erased())),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GethNodeResolver<F: TxFiller<Ethereum>, P: Provider<Ethereum>> {
|
pub struct GethNodeResolver {
|
||||||
id: u32,
|
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))]
|
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||||
fn chain_id(
|
fn chain_id(
|
||||||
&self,
|
&self,
|
||||||
@@ -648,12 +748,38 @@ mod tests {
|
|||||||
(context, node)
|
(context, node)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn shared_state() -> &'static (TestExecutionContext, GethNode) {
|
||||||
|
static STATE: LazyLock<(TestExecutionContext, GethNode)> = LazyLock::new(new_node);
|
||||||
|
&STATE
|
||||||
|
}
|
||||||
|
|
||||||
fn shared_node() -> &'static GethNode {
|
fn shared_node() -> &'static GethNode {
|
||||||
static NODE: LazyLock<(TestExecutionContext, GethNode)> = LazyLock::new(new_node);
|
&shared_state().1
|
||||||
&NODE.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]
|
#[test]
|
||||||
|
#[ignore = "Ignored since they take a long time to run"]
|
||||||
fn version_works() {
|
fn version_works() {
|
||||||
// Arrange
|
// Arrange
|
||||||
let node = shared_node();
|
let node = shared_node();
|
||||||
@@ -670,6 +796,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[ignore = "Ignored since they take a long time to run"]
|
||||||
async fn can_get_chain_id_from_node() {
|
async fn can_get_chain_id_from_node() {
|
||||||
// Arrange
|
// Arrange
|
||||||
let node = shared_node();
|
let node = shared_node();
|
||||||
@@ -683,6 +810,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[ignore = "Ignored since they take a long time to run"]
|
||||||
async fn can_get_gas_limit_from_node() {
|
async fn can_get_gas_limit_from_node() {
|
||||||
// Arrange
|
// Arrange
|
||||||
let node = shared_node();
|
let node = shared_node();
|
||||||
@@ -700,6 +828,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[ignore = "Ignored since they take a long time to run"]
|
||||||
async fn can_get_coinbase_from_node() {
|
async fn can_get_coinbase_from_node() {
|
||||||
// Arrange
|
// Arrange
|
||||||
let node = shared_node();
|
let node = shared_node();
|
||||||
@@ -717,6 +846,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[ignore = "Ignored since they take a long time to run"]
|
||||||
async fn can_get_block_difficulty_from_node() {
|
async fn can_get_block_difficulty_from_node() {
|
||||||
// Arrange
|
// Arrange
|
||||||
let node = shared_node();
|
let node = shared_node();
|
||||||
@@ -734,6 +864,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[ignore = "Ignored since they take a long time to run"]
|
||||||
async fn can_get_block_hash_from_node() {
|
async fn can_get_block_hash_from_node() {
|
||||||
// Arrange
|
// Arrange
|
||||||
let node = shared_node();
|
let node = shared_node();
|
||||||
@@ -751,6 +882,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[ignore = "Ignored since they take a long time to run"]
|
||||||
async fn can_get_block_timestamp_from_node() {
|
async fn can_get_block_timestamp_from_node() {
|
||||||
// Arrange
|
// Arrange
|
||||||
let node = shared_node();
|
let node = shared_node();
|
||||||
@@ -768,6 +900,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[ignore = "Ignored since they take a long time to run"]
|
||||||
async fn can_get_block_number_from_node() {
|
async fn can_get_block_number_from_node() {
|
||||||
// Arrange
|
// Arrange
|
||||||
let node = shared_node();
|
let node = shared_node();
|
||||||
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;
|
||||||
+203
-484
@@ -11,46 +11,45 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use alloy::{
|
use alloy::{
|
||||||
consensus::{BlockHeader, TxEnvelope},
|
|
||||||
eips::BlockNumberOrTag,
|
eips::BlockNumberOrTag,
|
||||||
genesis::{Genesis, GenesisAccount},
|
genesis::{Genesis, GenesisAccount},
|
||||||
network::{
|
network::{Ethereum, EthereumWallet, NetworkWallet},
|
||||||
Ethereum, EthereumWallet, Network, NetworkWallet, TransactionBuilder,
|
primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256},
|
||||||
TransactionBuilderError, UnbuiltTransactionError,
|
|
||||||
},
|
|
||||||
primitives::{
|
|
||||||
Address, B64, B256, BlockHash, BlockNumber, BlockTimestamp, Bloom, Bytes, StorageKey,
|
|
||||||
TxHash, U256,
|
|
||||||
},
|
|
||||||
providers::{
|
providers::{
|
||||||
Provider, ProviderBuilder,
|
Provider,
|
||||||
ext::DebugApi,
|
ext::DebugApi,
|
||||||
fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller},
|
fillers::{CachedNonceManager, ChainIdFiller, NonceFiller},
|
||||||
},
|
},
|
||||||
rpc::types::{
|
rpc::types::{
|
||||||
EIP1186AccountProofResponse, TransactionReceipt,
|
EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest,
|
||||||
eth::{Block, Header, Transaction},
|
trace::geth::{
|
||||||
trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame},
|
DiffMode, GethDebugTracingOptions, GethTrace, PreStateConfig, PreStateFrame,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
|
use futures::{FutureExt, Stream, StreamExt};
|
||||||
use revive_common::EVMVersion;
|
use revive_common::EVMVersion;
|
||||||
use revive_dt_common::fs::clear_directory;
|
use revive_dt_common::fs::clear_directory;
|
||||||
use revive_dt_format::traits::ResolverApi;
|
use revive_dt_format::traits::ResolverApi;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_json::{Value as JsonValue, json};
|
use serde_json::{Value as JsonValue, json};
|
||||||
use sp_core::crypto::Ss58Codec;
|
use sp_core::crypto::Ss58Codec;
|
||||||
use sp_runtime::AccountId32;
|
use sp_runtime::AccountId32;
|
||||||
|
|
||||||
use revive_dt_config::*;
|
use revive_dt_config::*;
|
||||||
use revive_dt_node_interaction::EthereumNode;
|
use revive_dt_node_interaction::{EthereumNode, MinedBlockInformation};
|
||||||
|
use subxt::{OnlineClient, SubstrateConfig};
|
||||||
|
use tokio::sync::OnceCell;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Node,
|
Node,
|
||||||
common::FallbackGasFiller,
|
constants::{CHAIN_ID, INITIAL_BALANCE},
|
||||||
constants::INITIAL_BALANCE,
|
helpers::{Process, ProcessReadinessWaitBehavior},
|
||||||
process::{Process, ProcessReadinessWaitBehavior},
|
provider_utils::{
|
||||||
|
ConcreteProvider, FallbackGasFiller, construct_concurrency_limited_provider,
|
||||||
|
execute_transaction,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
|
static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
|
||||||
@@ -59,6 +58,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
|
/// or the revive-dev-node which is done by changing the path and some of the other arguments passed
|
||||||
/// to the command.
|
/// to the command.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
||||||
pub struct SubstrateNode {
|
pub struct SubstrateNode {
|
||||||
id: u32,
|
id: u32,
|
||||||
node_binary: PathBuf,
|
node_binary: PathBuf,
|
||||||
@@ -71,11 +71,12 @@ pub struct SubstrateNode {
|
|||||||
eth_proxy_process: Option<Process>,
|
eth_proxy_process: Option<Process>,
|
||||||
wallet: Arc<EthereumWallet>,
|
wallet: Arc<EthereumWallet>,
|
||||||
nonce_manager: CachedNonceManager,
|
nonce_manager: CachedNonceManager,
|
||||||
chain_id_filler: ChainIdFiller,
|
provider: OnceCell<ConcreteProvider<Ethereum, Arc<EthereumWallet>>>,
|
||||||
|
consensus: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SubstrateNode {
|
impl SubstrateNode {
|
||||||
const BASE_DIRECTORY: &str = "Substrate";
|
const BASE_DIRECTORY: &str = "substrate";
|
||||||
const LOGS_DIRECTORY: &str = "logs";
|
const LOGS_DIRECTORY: &str = "logs";
|
||||||
const DATA_DIRECTORY: &str = "chains";
|
const DATA_DIRECTORY: &str = "chains";
|
||||||
|
|
||||||
@@ -94,6 +95,7 @@ impl SubstrateNode {
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
node_path: PathBuf,
|
node_path: PathBuf,
|
||||||
export_chainspec_command: &str,
|
export_chainspec_command: &str,
|
||||||
|
consensus: Option<String>,
|
||||||
context: impl AsRef<WorkingDirectoryConfiguration>
|
context: impl AsRef<WorkingDirectoryConfiguration>
|
||||||
+ AsRef<EthRpcConfiguration>
|
+ AsRef<EthRpcConfiguration>
|
||||||
+ AsRef<WalletConfiguration>,
|
+ AsRef<WalletConfiguration>,
|
||||||
@@ -121,8 +123,9 @@ impl SubstrateNode {
|
|||||||
substrate_process: None,
|
substrate_process: None,
|
||||||
eth_proxy_process: None,
|
eth_proxy_process: None,
|
||||||
wallet: wallet.clone(),
|
wallet: wallet.clone(),
|
||||||
chain_id_filler: Default::default(),
|
|
||||||
nonce_manager: Default::default(),
|
nonce_manager: Default::default(),
|
||||||
|
provider: Default::default(),
|
||||||
|
consensus,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,6 +147,7 @@ impl SubstrateNode {
|
|||||||
.arg(self.export_chainspec_command.as_str())
|
.arg(self.export_chainspec_command.as_str())
|
||||||
.arg("--chain")
|
.arg("--chain")
|
||||||
.arg("dev")
|
.arg("dev")
|
||||||
|
.env_remove("RUST_LOG")
|
||||||
.output()
|
.output()
|
||||||
.context("Failed to export the chain-spec")?;
|
.context("Failed to export the chain-spec")?;
|
||||||
|
|
||||||
@@ -219,7 +223,7 @@ impl SubstrateNode {
|
|||||||
self.logs_directory.as_path(),
|
self.logs_directory.as_path(),
|
||||||
self.node_binary.as_path(),
|
self.node_binary.as_path(),
|
||||||
|command, stdout_file, stderr_file| {
|
|command, stdout_file, stderr_file| {
|
||||||
command
|
let cmd = command
|
||||||
.arg("--dev")
|
.arg("--dev")
|
||||||
.arg("--chain")
|
.arg("--chain")
|
||||||
.arg(chainspec_path)
|
.arg(chainspec_path)
|
||||||
@@ -236,9 +240,16 @@ impl SubstrateNode {
|
|||||||
.arg("all")
|
.arg("all")
|
||||||
.arg("--rpc-max-connections")
|
.arg("--rpc-max-connections")
|
||||||
.arg(u32::MAX.to_string())
|
.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)
|
.env("RUST_LOG", Self::SUBSTRATE_LOG_ENV)
|
||||||
.stdout(stdout_file)
|
.stdout(stdout_file)
|
||||||
.stderr(stderr_file);
|
.stderr(stderr_file);
|
||||||
|
if let Some(consensus) = self.consensus.as_ref() {
|
||||||
|
cmd.arg("--consensus").arg(consensus.clone());
|
||||||
|
}
|
||||||
},
|
},
|
||||||
ProcessReadinessWaitBehavior::TimeBoundedWaitFunction {
|
ProcessReadinessWaitBehavior::TimeBoundedWaitFunction {
|
||||||
max_wait_duration: Duration::from_secs(30),
|
max_wait_duration: Duration::from_secs(30),
|
||||||
@@ -333,29 +344,29 @@ impl SubstrateNode {
|
|||||||
Ok(String::from_utf8_lossy(&output).trim().to_string())
|
Ok(String::from_utf8_lossy(&output).trim().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn provider(
|
async fn provider(&self) -> anyhow::Result<ConcreteProvider<Ethereum, Arc<EthereumWallet>>> {
|
||||||
&self,
|
self.provider
|
||||||
) -> anyhow::Result<
|
.get_or_try_init(|| async move {
|
||||||
FillProvider<impl TxFiller<ReviveNetwork>, impl Provider<ReviveNetwork>, ReviveNetwork>,
|
construct_concurrency_limited_provider::<Ethereum, _>(
|
||||||
> {
|
self.rpc_url.as_str(),
|
||||||
ProviderBuilder::new()
|
FallbackGasFiller::new(u64::MAX, 5_000_000_000, 1_000_000_000),
|
||||||
.disable_recommended_fillers()
|
ChainIdFiller::new(Some(CHAIN_ID)),
|
||||||
.network::<ReviveNetwork>()
|
NonceFiller::new(self.nonce_manager.clone()),
|
||||||
.filler(FallbackGasFiller::new(
|
self.wallet.clone(),
|
||||||
25_000_000,
|
)
|
||||||
1_000_000_000,
|
.await
|
||||||
1_000_000_000,
|
.context("Failed to construct the provider")
|
||||||
))
|
})
|
||||||
.filler(self.chain_id_filler.clone())
|
|
||||||
.filler(NonceFiller::new(self.nonce_manager.clone()))
|
|
||||||
.wallet(self.wallet.clone())
|
|
||||||
.connect(&self.rpc_url)
|
|
||||||
.await
|
.await
|
||||||
.map_err(Into::into)
|
.cloned()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EthereumNode for SubstrateNode {
|
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 {
|
fn id(&self) -> usize {
|
||||||
self.id as _
|
self.id as _
|
||||||
}
|
}
|
||||||
@@ -364,22 +375,48 @@ impl EthereumNode for SubstrateNode {
|
|||||||
&self.rpc_url
|
&self.rpc_url
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute_transaction(
|
fn submit_transaction(
|
||||||
&self,
|
&self,
|
||||||
transaction: alloy::rpc::types::TransactionRequest,
|
transaction: TransactionRequest,
|
||||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<TransactionReceipt>> + '_>> {
|
) -> Pin<Box<dyn Future<Output = anyhow::Result<TxHash>> + '_>> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let receipt = self
|
let provider = self
|
||||||
.provider()
|
.provider()
|
||||||
.await
|
.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)
|
.send_transaction(transaction)
|
||||||
.await
|
.await
|
||||||
.context("Failed to submit transaction to substrate proxy")?
|
.context("Failed to submit the transaction through the provider")?;
|
||||||
.get_receipt()
|
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
|
.await
|
||||||
.context("Failed to fetch transaction receipt from substrate proxy")?;
|
.context("Failed to create provider for getting the receipt")?
|
||||||
Ok(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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,8 +424,7 @@ impl EthereumNode for SubstrateNode {
|
|||||||
&self,
|
&self,
|
||||||
tx_hash: TxHash,
|
tx_hash: TxHash,
|
||||||
trace_options: GethDebugTracingOptions,
|
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 {
|
Box::pin(async move {
|
||||||
self.provider()
|
self.provider()
|
||||||
.await
|
.await
|
||||||
@@ -463,16 +499,109 @@ impl EthereumNode for SubstrateNode {
|
|||||||
fn evm_version(&self) -> EVMVersion {
|
fn evm_version(&self) -> EVMVersion {
|
||||||
EVMVersion::Cancun
|
EVMVersion::Cancun
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn subscribe_to_full_blocks_information(
|
||||||
|
&self,
|
||||||
|
) -> Pin<
|
||||||
|
Box<
|
||||||
|
dyn Future<Output = anyhow::Result<Pin<Box<dyn Stream<Item = MinedBlockInformation>>>>>
|
||||||
|
+ '_,
|
||||||
|
>,
|
||||||
|
> {
|
||||||
|
#[subxt::subxt(runtime_metadata_path = "../../assets/revive_metadata.scale")]
|
||||||
|
pub mod revive {}
|
||||||
|
|
||||||
|
Box::pin(async move {
|
||||||
|
let substrate_rpc_port = Self::BASE_SUBSTRATE_RPC_PORT + self.id as u16;
|
||||||
|
let substrate_rpc_url = format!("ws://127.0.0.1:{substrate_rpc_port}");
|
||||||
|
let api = OnlineClient::<SubstrateConfig>::from_url(substrate_rpc_url)
|
||||||
|
.await
|
||||||
|
.context("Failed to create subxt rpc client")?;
|
||||||
|
let provider = self.provider().await.context("Failed to create provider")?;
|
||||||
|
|
||||||
|
let block_stream = api
|
||||||
|
.blocks()
|
||||||
|
.subscribe_all()
|
||||||
|
.await
|
||||||
|
.context("Failed to subscribe to blocks")?;
|
||||||
|
|
||||||
|
let mined_block_information_stream = block_stream.filter_map(move |block| {
|
||||||
|
let api = api.clone();
|
||||||
|
let provider = provider.clone();
|
||||||
|
|
||||||
|
async move {
|
||||||
|
let substrate_block = block.ok()?;
|
||||||
|
let revive_block = provider
|
||||||
|
.get_block_by_number(
|
||||||
|
BlockNumberOrTag::Number(substrate_block.number() as _),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("TODO: Remove")
|
||||||
|
.expect("TODO: Remove");
|
||||||
|
|
||||||
|
let used = api
|
||||||
|
.storage()
|
||||||
|
.at(substrate_block.reference())
|
||||||
|
.fetch_or_default(&revive::storage().system().block_weight())
|
||||||
|
.await
|
||||||
|
.expect("TODO: Remove");
|
||||||
|
|
||||||
|
let block_ref_time = (used.normal.ref_time as u128)
|
||||||
|
+ (used.operational.ref_time as u128)
|
||||||
|
+ (used.mandatory.ref_time as u128);
|
||||||
|
let block_proof_size = (used.normal.proof_size as u128)
|
||||||
|
+ (used.operational.proof_size as u128)
|
||||||
|
+ (used.mandatory.proof_size as u128);
|
||||||
|
|
||||||
|
let limits = api
|
||||||
|
.constants()
|
||||||
|
.at(&revive::constants().system().block_weights())
|
||||||
|
.expect("TODO: Remove");
|
||||||
|
|
||||||
|
let max_ref_time = limits.max_block.ref_time;
|
||||||
|
let max_proof_size = limits.max_block.proof_size;
|
||||||
|
|
||||||
|
Some(MinedBlockInformation {
|
||||||
|
block_number: substrate_block.number() as _,
|
||||||
|
block_timestamp: revive_block.header.timestamp,
|
||||||
|
mined_gas: revive_block.header.gas_used as _,
|
||||||
|
block_gas_limit: revive_block.header.gas_limit as _,
|
||||||
|
transaction_hashes: revive_block
|
||||||
|
.transactions
|
||||||
|
.into_hashes()
|
||||||
|
.as_hashes()
|
||||||
|
.expect("Must be hashes")
|
||||||
|
.to_vec(),
|
||||||
|
ref_time: block_ref_time,
|
||||||
|
max_ref_time,
|
||||||
|
proof_size: block_proof_size,
|
||||||
|
max_proof_size,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Box::pin(mined_block_information_stream)
|
||||||
|
as Pin<Box<dyn Stream<Item = MinedBlockInformation>>>)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn provider(
|
||||||
|
&self,
|
||||||
|
) -> Pin<Box<dyn Future<Output = anyhow::Result<alloy::providers::DynProvider<Ethereum>>> + '_>>
|
||||||
|
{
|
||||||
|
Box::pin(
|
||||||
|
self.provider()
|
||||||
|
.map(|provider| provider.map(|provider| provider.erased())),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SubstrateNodeResolver<F: TxFiller<ReviveNetwork>, P: Provider<ReviveNetwork>> {
|
pub struct SubstrateNodeResolver {
|
||||||
id: u32,
|
id: u32,
|
||||||
provider: FillProvider<F, P, ReviveNetwork>,
|
provider: ConcreteProvider<Ethereum, Arc<EthereumWallet>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<F: TxFiller<ReviveNetwork>, P: Provider<ReviveNetwork>> ResolverApi
|
impl ResolverApi for SubstrateNodeResolver {
|
||||||
for SubstrateNodeResolver<F, P>
|
|
||||||
{
|
|
||||||
#[instrument(level = "info", skip_all, fields(substrate_node_id = self.id))]
|
#[instrument(level = "info", skip_all, fields(substrate_node_id = self.id))]
|
||||||
fn chain_id(
|
fn chain_id(
|
||||||
&self,
|
&self,
|
||||||
@@ -633,430 +762,6 @@ impl Drop for SubstrateNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
pub struct ReviveNetwork;
|
|
||||||
|
|
||||||
impl Network for ReviveNetwork {
|
|
||||||
type TxType = <Ethereum as Network>::TxType;
|
|
||||||
|
|
||||||
type TxEnvelope = <Ethereum as Network>::TxEnvelope;
|
|
||||||
|
|
||||||
type UnsignedTx = <Ethereum as Network>::UnsignedTx;
|
|
||||||
|
|
||||||
type ReceiptEnvelope = <Ethereum as Network>::ReceiptEnvelope;
|
|
||||||
|
|
||||||
type Header = ReviveHeader;
|
|
||||||
|
|
||||||
type TransactionRequest = <Ethereum as Network>::TransactionRequest;
|
|
||||||
|
|
||||||
type TransactionResponse = <Ethereum as Network>::TransactionResponse;
|
|
||||||
|
|
||||||
type ReceiptResponse = <Ethereum as Network>::ReceiptResponse;
|
|
||||||
|
|
||||||
type HeaderResponse = Header<ReviveHeader>;
|
|
||||||
|
|
||||||
type BlockResponse = Block<Transaction<TxEnvelope>, Header<ReviveHeader>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TransactionBuilder<ReviveNetwork> for <Ethereum as Network>::TransactionRequest {
|
|
||||||
fn chain_id(&self) -> Option<alloy::primitives::ChainId> {
|
|
||||||
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::chain_id(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_chain_id(&mut self, chain_id: alloy::primitives::ChainId) {
|
|
||||||
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::set_chain_id(
|
|
||||||
self, chain_id,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn nonce(&self) -> Option<u64> {
|
|
||||||
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::nonce(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_nonce(&mut self, nonce: u64) {
|
|
||||||
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::set_nonce(
|
|
||||||
self, nonce,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn take_nonce(&mut self) -> Option<u64> {
|
|
||||||
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::take_nonce(
|
|
||||||
self,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn input(&self) -> Option<&alloy::primitives::Bytes> {
|
|
||||||
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::input(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_input<T: Into<alloy::primitives::Bytes>>(&mut self, input: T) {
|
|
||||||
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::set_input(
|
|
||||||
self, input,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from(&self) -> Option<Address> {
|
|
||||||
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::from(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_from(&mut self, from: Address) {
|
|
||||||
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::set_from(
|
|
||||||
self, from,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn kind(&self) -> Option<alloy::primitives::TxKind> {
|
|
||||||
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::kind(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clear_kind(&mut self) {
|
|
||||||
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::clear_kind(
|
|
||||||
self,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_kind(&mut self, kind: alloy::primitives::TxKind) {
|
|
||||||
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::set_kind(
|
|
||||||
self, kind,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn value(&self) -> Option<alloy::primitives::U256> {
|
|
||||||
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::value(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_value(&mut self, value: alloy::primitives::U256) {
|
|
||||||
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::set_value(
|
|
||||||
self, value,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn gas_price(&self) -> Option<u128> {
|
|
||||||
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::gas_price(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_gas_price(&mut self, gas_price: u128) {
|
|
||||||
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::set_gas_price(
|
|
||||||
self, gas_price,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn max_fee_per_gas(&self) -> Option<u128> {
|
|
||||||
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::max_fee_per_gas(
|
|
||||||
self,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_max_fee_per_gas(&mut self, max_fee_per_gas: u128) {
|
|
||||||
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::set_max_fee_per_gas(
|
|
||||||
self, max_fee_per_gas
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn max_priority_fee_per_gas(&self) -> Option<u128> {
|
|
||||||
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::max_priority_fee_per_gas(
|
|
||||||
self,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_max_priority_fee_per_gas(&mut self, max_priority_fee_per_gas: u128) {
|
|
||||||
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::set_max_priority_fee_per_gas(
|
|
||||||
self, max_priority_fee_per_gas
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn gas_limit(&self) -> Option<u64> {
|
|
||||||
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::gas_limit(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_gas_limit(&mut self, gas_limit: u64) {
|
|
||||||
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::set_gas_limit(
|
|
||||||
self, gas_limit,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn access_list(&self) -> Option<&alloy::rpc::types::AccessList> {
|
|
||||||
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::access_list(
|
|
||||||
self,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_access_list(&mut self, access_list: alloy::rpc::types::AccessList) {
|
|
||||||
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::set_access_list(
|
|
||||||
self,
|
|
||||||
access_list,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn complete_type(
|
|
||||||
&self,
|
|
||||||
ty: <ReviveNetwork as Network>::TxType,
|
|
||||||
) -> Result<(), Vec<&'static str>> {
|
|
||||||
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::complete_type(
|
|
||||||
self, ty,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn can_submit(&self) -> bool {
|
|
||||||
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::can_submit(
|
|
||||||
self,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn can_build(&self) -> bool {
|
|
||||||
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::can_build(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn output_tx_type(&self) -> <ReviveNetwork as Network>::TxType {
|
|
||||||
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::output_tx_type(
|
|
||||||
self,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn output_tx_type_checked(&self) -> Option<<ReviveNetwork as Network>::TxType> {
|
|
||||||
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::output_tx_type_checked(
|
|
||||||
self,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prep_for_submission(&mut self) {
|
|
||||||
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::prep_for_submission(
|
|
||||||
self,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_unsigned(
|
|
||||||
self,
|
|
||||||
) -> alloy::network::BuildResult<<ReviveNetwork as Network>::UnsignedTx, ReviveNetwork> {
|
|
||||||
let result = <<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::build_unsigned(
|
|
||||||
self,
|
|
||||||
);
|
|
||||||
match result {
|
|
||||||
Ok(unsigned_tx) => Ok(unsigned_tx),
|
|
||||||
Err(UnbuiltTransactionError { request, error }) => {
|
|
||||||
Err(UnbuiltTransactionError::<ReviveNetwork> {
|
|
||||||
request,
|
|
||||||
error: match error {
|
|
||||||
TransactionBuilderError::InvalidTransactionRequest(tx_type, items) => {
|
|
||||||
TransactionBuilderError::InvalidTransactionRequest(tx_type, items)
|
|
||||||
}
|
|
||||||
TransactionBuilderError::UnsupportedSignatureType => {
|
|
||||||
TransactionBuilderError::UnsupportedSignatureType
|
|
||||||
}
|
|
||||||
TransactionBuilderError::Signer(error) => {
|
|
||||||
TransactionBuilderError::Signer(error)
|
|
||||||
}
|
|
||||||
TransactionBuilderError::Custom(error) => {
|
|
||||||
TransactionBuilderError::Custom(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn build<W: alloy::network::NetworkWallet<ReviveNetwork>>(
|
|
||||||
self,
|
|
||||||
wallet: &W,
|
|
||||||
) -> Result<<ReviveNetwork as Network>::TxEnvelope, TransactionBuilderError<ReviveNetwork>>
|
|
||||||
{
|
|
||||||
Ok(wallet.sign_request(self).await?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct ReviveHeader {
|
|
||||||
/// The Keccak 256-bit hash of the parent
|
|
||||||
/// block’s header, in its entirety; formally Hp.
|
|
||||||
pub parent_hash: B256,
|
|
||||||
/// The Keccak 256-bit hash of the ommers list portion of this block; formally Ho.
|
|
||||||
#[serde(rename = "sha3Uncles", alias = "ommersHash")]
|
|
||||||
pub ommers_hash: B256,
|
|
||||||
/// The 160-bit address to which all fees collected from the successful mining of this block
|
|
||||||
/// be transferred; formally Hc.
|
|
||||||
#[serde(rename = "miner", alias = "beneficiary")]
|
|
||||||
pub beneficiary: Address,
|
|
||||||
/// The Keccak 256-bit hash of the root node of the state trie, after all transactions are
|
|
||||||
/// executed and finalisations applied; formally Hr.
|
|
||||||
pub state_root: B256,
|
|
||||||
/// The Keccak 256-bit hash of the root node of the trie structure populated with each
|
|
||||||
/// transaction in the transactions list portion of the block; formally Ht.
|
|
||||||
pub transactions_root: B256,
|
|
||||||
/// The Keccak 256-bit hash of the root node of the trie structure populated with the receipts
|
|
||||||
/// of each transaction in the transactions list portion of the block; formally He.
|
|
||||||
pub receipts_root: B256,
|
|
||||||
/// The Bloom filter composed from indexable information (logger address and log topics)
|
|
||||||
/// contained in each log entry from the receipt of each transaction in the transactions list;
|
|
||||||
/// formally Hb.
|
|
||||||
pub logs_bloom: Bloom,
|
|
||||||
/// A scalar value corresponding to the difficulty level of this block. This can be calculated
|
|
||||||
/// from the previous block’s difficulty level and the timestamp; formally Hd.
|
|
||||||
pub difficulty: U256,
|
|
||||||
/// A scalar value equal to the number of ancestor blocks. The genesis block has a number of
|
|
||||||
/// zero; formally Hi.
|
|
||||||
#[serde(with = "alloy::serde::quantity")]
|
|
||||||
pub number: BlockNumber,
|
|
||||||
/// A scalar value equal to the current limit of gas expenditure per block; formally Hl.
|
|
||||||
// This is the main difference over the Ethereum network implementation. We use u128 here and
|
|
||||||
// not u64.
|
|
||||||
#[serde(with = "alloy::serde::quantity")]
|
|
||||||
pub gas_limit: u128,
|
|
||||||
/// A scalar value equal to the total gas used in transactions in this block; formally Hg.
|
|
||||||
#[serde(with = "alloy::serde::quantity")]
|
|
||||||
pub gas_used: u64,
|
|
||||||
/// A scalar value equal to the reasonable output of Unix’s time() at this block’s inception;
|
|
||||||
/// formally Hs.
|
|
||||||
#[serde(with = "alloy::serde::quantity")]
|
|
||||||
pub timestamp: u64,
|
|
||||||
/// An arbitrary byte array containing data relevant to this block. This must be 32 bytes or
|
|
||||||
/// fewer; formally Hx.
|
|
||||||
pub extra_data: Bytes,
|
|
||||||
/// A 256-bit hash which, combined with the
|
|
||||||
/// nonce, proves that a sufficient amount of computation has been carried out on this block;
|
|
||||||
/// formally Hm.
|
|
||||||
pub mix_hash: B256,
|
|
||||||
/// A 64-bit value which, combined with the mixhash, proves that a sufficient amount of
|
|
||||||
/// computation has been carried out on this block; formally Hn.
|
|
||||||
pub nonce: B64,
|
|
||||||
/// A scalar representing EIP1559 base fee which can move up or down each block according
|
|
||||||
/// to a formula which is a function of gas used in parent block and gas target
|
|
||||||
/// (block gas limit divided by elasticity multiplier) of parent block.
|
|
||||||
/// The algorithm results in the base fee per gas increasing when blocks are
|
|
||||||
/// above the gas target, and decreasing when blocks are below the gas target. The base fee per
|
|
||||||
/// gas is burned.
|
|
||||||
#[serde(
|
|
||||||
default,
|
|
||||||
with = "alloy::serde::quantity::opt",
|
|
||||||
skip_serializing_if = "Option::is_none"
|
|
||||||
)]
|
|
||||||
pub base_fee_per_gas: Option<u64>,
|
|
||||||
/// The Keccak 256-bit hash of the withdrawals list portion of this block.
|
|
||||||
/// <https://eips.ethereum.org/EIPS/eip-4895>
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub withdrawals_root: Option<B256>,
|
|
||||||
/// The total amount of blob gas consumed by the transactions within the block, added in
|
|
||||||
/// EIP-4844.
|
|
||||||
#[serde(
|
|
||||||
default,
|
|
||||||
with = "alloy::serde::quantity::opt",
|
|
||||||
skip_serializing_if = "Option::is_none"
|
|
||||||
)]
|
|
||||||
pub blob_gas_used: Option<u64>,
|
|
||||||
/// A running total of blob gas consumed in excess of the target, prior to the block. Blocks
|
|
||||||
/// with above-target blob gas consumption increase this value, blocks with below-target blob
|
|
||||||
/// gas consumption decrease it (bounded at 0). This was added in EIP-4844.
|
|
||||||
#[serde(
|
|
||||||
default,
|
|
||||||
with = "alloy::serde::quantity::opt",
|
|
||||||
skip_serializing_if = "Option::is_none"
|
|
||||||
)]
|
|
||||||
pub excess_blob_gas: Option<u64>,
|
|
||||||
/// The hash of the parent beacon block's root is included in execution blocks, as proposed by
|
|
||||||
/// EIP-4788.
|
|
||||||
///
|
|
||||||
/// This enables trust-minimized access to consensus state, supporting staking pools, bridges,
|
|
||||||
/// and more.
|
|
||||||
///
|
|
||||||
/// The beacon roots contract handles root storage, enhancing Ethereum's functionalities.
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub parent_beacon_block_root: Option<B256>,
|
|
||||||
/// The Keccak 256-bit hash of the an RLP encoded list with each
|
|
||||||
/// [EIP-7685] request in the block body.
|
|
||||||
///
|
|
||||||
/// [EIP-7685]: https://eips.ethereum.org/EIPS/eip-7685
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub requests_hash: Option<B256>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BlockHeader for ReviveHeader {
|
|
||||||
fn parent_hash(&self) -> B256 {
|
|
||||||
self.parent_hash
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ommers_hash(&self) -> B256 {
|
|
||||||
self.ommers_hash
|
|
||||||
}
|
|
||||||
|
|
||||||
fn beneficiary(&self) -> Address {
|
|
||||||
self.beneficiary
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state_root(&self) -> B256 {
|
|
||||||
self.state_root
|
|
||||||
}
|
|
||||||
|
|
||||||
fn transactions_root(&self) -> B256 {
|
|
||||||
self.transactions_root
|
|
||||||
}
|
|
||||||
|
|
||||||
fn receipts_root(&self) -> B256 {
|
|
||||||
self.receipts_root
|
|
||||||
}
|
|
||||||
|
|
||||||
fn withdrawals_root(&self) -> Option<B256> {
|
|
||||||
self.withdrawals_root
|
|
||||||
}
|
|
||||||
|
|
||||||
fn logs_bloom(&self) -> Bloom {
|
|
||||||
self.logs_bloom
|
|
||||||
}
|
|
||||||
|
|
||||||
fn difficulty(&self) -> U256 {
|
|
||||||
self.difficulty
|
|
||||||
}
|
|
||||||
|
|
||||||
fn number(&self) -> BlockNumber {
|
|
||||||
self.number
|
|
||||||
}
|
|
||||||
|
|
||||||
// There's sadly nothing that we can do about this. We're required to implement this trait on
|
|
||||||
// any type that represents a header and the gas limit type used here is a u64.
|
|
||||||
fn gas_limit(&self) -> u64 {
|
|
||||||
self.gas_limit.try_into().unwrap_or(u64::MAX)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn gas_used(&self) -> u64 {
|
|
||||||
self.gas_used
|
|
||||||
}
|
|
||||||
|
|
||||||
fn timestamp(&self) -> u64 {
|
|
||||||
self.timestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mix_hash(&self) -> Option<B256> {
|
|
||||||
Some(self.mix_hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn nonce(&self) -> Option<B64> {
|
|
||||||
Some(self.nonce)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn base_fee_per_gas(&self) -> Option<u64> {
|
|
||||||
self.base_fee_per_gas
|
|
||||||
}
|
|
||||||
|
|
||||||
fn blob_gas_used(&self) -> Option<u64> {
|
|
||||||
self.blob_gas_used
|
|
||||||
}
|
|
||||||
|
|
||||||
fn excess_blob_gas(&self) -> Option<u64> {
|
|
||||||
self.excess_blob_gas
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parent_beacon_block_root(&self) -> Option<B256> {
|
|
||||||
self.parent_beacon_block_root
|
|
||||||
}
|
|
||||||
|
|
||||||
fn requests_hash(&self) -> Option<B256> {
|
|
||||||
self.requests_hash
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extra_data(&self) -> &Bytes {
|
|
||||||
&self.extra_data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use alloy::rpc::types::TransactionRequest;
|
use alloy::rpc::types::TransactionRequest;
|
||||||
@@ -1068,9 +773,7 @@ mod tests {
|
|||||||
use crate::Node;
|
use crate::Node;
|
||||||
|
|
||||||
fn test_config() -> TestExecutionContext {
|
fn test_config() -> TestExecutionContext {
|
||||||
let mut context = TestExecutionContext::default();
|
TestExecutionContext::default()
|
||||||
context.kitchensink_configuration.use_kitchensink = true;
|
|
||||||
context
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_node() -> (TestExecutionContext, SubstrateNode) {
|
fn new_node() -> (TestExecutionContext, SubstrateNode) {
|
||||||
@@ -1096,6 +799,7 @@ mod tests {
|
|||||||
let mut node = SubstrateNode::new(
|
let mut node = SubstrateNode::new(
|
||||||
context.kitchensink_configuration.path.clone(),
|
context.kitchensink_configuration.path.clone(),
|
||||||
SubstrateNode::KITCHENSINK_EXPORT_CHAINSPEC_COMMAND,
|
SubstrateNode::KITCHENSINK_EXPORT_CHAINSPEC_COMMAND,
|
||||||
|
None,
|
||||||
&context,
|
&context,
|
||||||
);
|
);
|
||||||
node.init(context.genesis_configuration.genesis().unwrap().clone())
|
node.init(context.genesis_configuration.genesis().unwrap().clone())
|
||||||
@@ -1142,6 +846,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[ignore = "Ignored since they take a long time to run"]
|
||||||
fn test_init_generates_chainspec_with_balances() {
|
fn test_init_generates_chainspec_with_balances() {
|
||||||
let genesis_content = r#"
|
let genesis_content = r#"
|
||||||
{
|
{
|
||||||
@@ -1160,6 +865,7 @@ mod tests {
|
|||||||
let mut dummy_node = SubstrateNode::new(
|
let mut dummy_node = SubstrateNode::new(
|
||||||
context.kitchensink_configuration.path.clone(),
|
context.kitchensink_configuration.path.clone(),
|
||||||
SubstrateNode::KITCHENSINK_EXPORT_CHAINSPEC_COMMAND,
|
SubstrateNode::KITCHENSINK_EXPORT_CHAINSPEC_COMMAND,
|
||||||
|
None,
|
||||||
&context,
|
&context,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1195,6 +901,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[ignore = "Ignored since they take a long time to run"]
|
||||||
fn test_parse_genesis_alloc() {
|
fn test_parse_genesis_alloc() {
|
||||||
// Create test genesis file
|
// Create test genesis file
|
||||||
let genesis_json = r#"
|
let genesis_json = r#"
|
||||||
@@ -1211,6 +918,7 @@ mod tests {
|
|||||||
let node = SubstrateNode::new(
|
let node = SubstrateNode::new(
|
||||||
context.kitchensink_configuration.path.clone(),
|
context.kitchensink_configuration.path.clone(),
|
||||||
SubstrateNode::KITCHENSINK_EXPORT_CHAINSPEC_COMMAND,
|
SubstrateNode::KITCHENSINK_EXPORT_CHAINSPEC_COMMAND,
|
||||||
|
None,
|
||||||
&context,
|
&context,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1237,6 +945,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[ignore = "Ignored since they take a long time to run"]
|
||||||
fn print_eth_to_substrate_mappings() {
|
fn print_eth_to_substrate_mappings() {
|
||||||
let eth_addresses = vec![
|
let eth_addresses = vec![
|
||||||
"0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1",
|
"0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1",
|
||||||
@@ -1252,6 +961,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[ignore = "Ignored since they take a long time to run"]
|
||||||
fn test_eth_to_substrate_address() {
|
fn test_eth_to_substrate_address() {
|
||||||
let cases = vec![
|
let cases = vec![
|
||||||
(
|
(
|
||||||
@@ -1282,6 +992,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[ignore = "Ignored since they take a long time to run"]
|
||||||
fn version_works() {
|
fn version_works() {
|
||||||
let node = shared_node();
|
let node = shared_node();
|
||||||
|
|
||||||
@@ -1294,6 +1005,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[ignore = "Ignored since they take a long time to run"]
|
||||||
fn eth_rpc_version_works() {
|
fn eth_rpc_version_works() {
|
||||||
let node = shared_node();
|
let node = shared_node();
|
||||||
|
|
||||||
@@ -1306,6 +1018,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[ignore = "Ignored since they take a long time to run"]
|
||||||
async fn can_get_chain_id_from_node() {
|
async fn can_get_chain_id_from_node() {
|
||||||
// Arrange
|
// Arrange
|
||||||
let node = shared_node();
|
let node = shared_node();
|
||||||
@@ -1319,6 +1032,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[ignore = "Ignored since they take a long time to run"]
|
||||||
async fn can_get_gas_limit_from_node() {
|
async fn can_get_gas_limit_from_node() {
|
||||||
// Arrange
|
// Arrange
|
||||||
let node = shared_node();
|
let node = shared_node();
|
||||||
@@ -1336,6 +1050,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[ignore = "Ignored since they take a long time to run"]
|
||||||
async fn can_get_coinbase_from_node() {
|
async fn can_get_coinbase_from_node() {
|
||||||
// Arrange
|
// Arrange
|
||||||
let node = shared_node();
|
let node = shared_node();
|
||||||
@@ -1353,6 +1068,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[ignore = "Ignored since they take a long time to run"]
|
||||||
async fn can_get_block_difficulty_from_node() {
|
async fn can_get_block_difficulty_from_node() {
|
||||||
// Arrange
|
// Arrange
|
||||||
let node = shared_node();
|
let node = shared_node();
|
||||||
@@ -1370,6 +1086,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[ignore = "Ignored since they take a long time to run"]
|
||||||
async fn can_get_block_hash_from_node() {
|
async fn can_get_block_hash_from_node() {
|
||||||
// Arrange
|
// Arrange
|
||||||
let node = shared_node();
|
let node = shared_node();
|
||||||
@@ -1387,6 +1104,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[ignore = "Ignored since they take a long time to run"]
|
||||||
async fn can_get_block_timestamp_from_node() {
|
async fn can_get_block_timestamp_from_node() {
|
||||||
// Arrange
|
// Arrange
|
||||||
let node = shared_node();
|
let node = shared_node();
|
||||||
@@ -1404,6 +1122,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[ignore = "Ignored since they take a long time to run"]
|
||||||
async fn can_get_block_number_from_node() {
|
async fn can_get_block_number_from_node() {
|
||||||
// Arrange
|
// Arrange
|
||||||
let node = shared_node();
|
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,
|
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)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct FallbackGasFiller {
|
pub struct FallbackGasFiller {
|
||||||
inner: GasFiller,
|
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
|
impl<N> TxFiller<N> for FallbackGasFiller
|
||||||
where
|
where
|
||||||
N: Network,
|
N: Network,
|
||||||
@@ -50,8 +60,6 @@ where
|
|||||||
provider: &P,
|
provider: &P,
|
||||||
tx: &<N as Network>::TransactionRequest,
|
tx: &<N as Network>::TransactionRequest,
|
||||||
) -> TransportResult<Self::Fillable> {
|
) -> TransportResult<Self::Fillable> {
|
||||||
// Try to fetch GasFiller’s “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 {
|
match self.inner.prepare(provider, tx).await {
|
||||||
Ok(fill) => Ok(Some(fill)),
|
Ok(fill) => Ok(Some(fill)),
|
||||||
Err(_) => Ok(None),
|
Err(_) => Ok(None),
|
||||||
@@ -64,8 +72,17 @@ where
|
|||||||
mut tx: alloy::providers::SendableTx<N>,
|
mut tx: alloy::providers::SendableTx<N>,
|
||||||
) -> TransportResult<SendableTx<N>> {
|
) -> TransportResult<SendableTx<N>> {
|
||||||
if let Some(fill) = fillable {
|
if let Some(fill) = fillable {
|
||||||
// our inner GasFiller succeeded — use it
|
let mut tx = self.inner.fill(fill, tx).await?;
|
||||||
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 {
|
} else {
|
||||||
if let Some(builder) = tx.as_mut_builder() {
|
if let Some(builder) = tx.as_mut_builder() {
|
||||||
builder.set_gas_limit(self.default_gas_limit);
|
builder.set_gas_limit(self.default_gas_limit);
|
||||||
@@ -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::*;
|
||||||
@@ -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(1000));
|
||||||
|
|
||||||
|
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}"))
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ revive-dt-config = { workspace = true }
|
|||||||
revive-dt-format = { workspace = true }
|
revive-dt-format = { workspace = true }
|
||||||
revive-dt-compiler = { workspace = true }
|
revive-dt-compiler = { workspace = true }
|
||||||
|
|
||||||
alloy-primitives = { workspace = true }
|
alloy = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
paste = { workspace = true }
|
paste = { workspace = true }
|
||||||
indexmap = { workspace = true, features = ["serde"] }
|
indexmap = { workspace = true, features = ["serde"] }
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use std::{
|
|||||||
time::{SystemTime, UNIX_EPOCH},
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
use alloy_primitives::Address;
|
use alloy::primitives::Address;
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use revive_dt_common::types::PlatformIdentifier;
|
use revive_dt_common::types::PlatformIdentifier;
|
||||||
@@ -16,7 +16,7 @@ use revive_dt_compiler::{CompilerInput, CompilerOutput, Mode};
|
|||||||
use revive_dt_config::Context;
|
use revive_dt_config::Context;
|
||||||
use revive_dt_format::{case::CaseIdx, corpus::Corpus, metadata::ContractInstance};
|
use revive_dt_format::{case::CaseIdx, corpus::Corpus, metadata::ContractInstance};
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_with::{DisplayFromStr, serde_as};
|
use serde_with::{DisplayFromStr, serde_as};
|
||||||
use tokio::sync::{
|
use tokio::sync::{
|
||||||
broadcast::{Sender, channel},
|
broadcast::{Sender, channel},
|
||||||
@@ -106,6 +106,10 @@ impl ReportAggregator {
|
|||||||
RunnerEvent::ContractDeployed(event) => {
|
RunnerEvent::ContractDeployed(event) => {
|
||||||
self.handle_contract_deployed_event(*event);
|
self.handle_contract_deployed_event(*event);
|
||||||
}
|
}
|
||||||
|
RunnerEvent::Completion(event) => {
|
||||||
|
self.handle_completion(*event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
debug!("Report aggregation completed");
|
debug!("Report aggregation completed");
|
||||||
@@ -382,6 +386,10 @@ impl ReportAggregator {
|
|||||||
.insert(event.contract_instance, event.address);
|
.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 {
|
fn test_case_report(&mut self, specifier: &TestSpecifier) -> &mut TestCaseReport {
|
||||||
self.report
|
self.report
|
||||||
.test_case_information
|
.test_case_information
|
||||||
@@ -407,7 +415,7 @@ impl ReportAggregator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct Report {
|
pub struct Report {
|
||||||
/// The context that the tool was started up with.
|
/// The context that the tool was started up with.
|
||||||
pub context: Context,
|
pub context: Context,
|
||||||
@@ -432,7 +440,7 @@ impl Report {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Default)]
|
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||||
pub struct TestCaseReport {
|
pub struct TestCaseReport {
|
||||||
/// Information on the status of the test case and whether it succeeded, failed, or was ignored.
|
/// Information on the status of the test case and whether it succeeded, failed, or was ignored.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -443,7 +451,7 @@ pub struct TestCaseReport {
|
|||||||
|
|
||||||
/// Information related to the status of the test. Could be that the test succeeded, failed, or that
|
/// Information related to the status of the test. Could be that the test succeeded, failed, or that
|
||||||
/// it was ignored.
|
/// it was ignored.
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[serde(tag = "status")]
|
#[serde(tag = "status")]
|
||||||
pub enum TestCaseStatus {
|
pub enum TestCaseStatus {
|
||||||
/// The test case succeeded.
|
/// The test case succeeded.
|
||||||
@@ -467,7 +475,7 @@ pub enum TestCaseStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Information related to the platform node that's being used to execute the step.
|
/// Information related to the platform node that's being used to execute the step.
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct TestCaseNodeInformation {
|
pub struct TestCaseNodeInformation {
|
||||||
/// The ID of the node that this case is being executed on.
|
/// The ID of the node that this case is being executed on.
|
||||||
pub id: usize,
|
pub id: usize,
|
||||||
@@ -478,7 +486,7 @@ pub struct TestCaseNodeInformation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Execution information tied to the platform.
|
/// Execution information tied to the platform.
|
||||||
#[derive(Clone, Debug, Default, Serialize)]
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||||
pub struct ExecutionInformation {
|
pub struct ExecutionInformation {
|
||||||
/// Information related to the node assigned to this test case.
|
/// Information related to the node assigned to this test case.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -498,7 +506,7 @@ pub struct ExecutionInformation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Information related to compilation
|
/// Information related to compilation
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[serde(tag = "status")]
|
#[serde(tag = "status")]
|
||||||
pub enum CompilationStatus {
|
pub enum CompilationStatus {
|
||||||
/// The compilation was successful.
|
/// The compilation was successful.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
use std::{collections::BTreeMap, path::PathBuf, sync::Arc};
|
use std::{collections::BTreeMap, path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
use alloy_primitives::Address;
|
use alloy::primitives::Address;
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use revive_dt_common::types::PlatformIdentifier;
|
use revive_dt_common::types::PlatformIdentifier;
|
||||||
@@ -613,6 +613,8 @@ define_event! {
|
|||||||
/// The address of the contract.
|
/// The address of the contract.
|
||||||
address: Address
|
address: Address
|
||||||
},
|
},
|
||||||
|
/// Reports the completion of the run.
|
||||||
|
Completion {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-5
@@ -75,7 +75,9 @@ ABSOLUTE_PATH=$(realpath "$TEST_REPO_DIR/fixtures/solidity/")
|
|||||||
cat > "$CORPUS_FILE" << EOF
|
cat > "$CORPUS_FILE" << EOF
|
||||||
{
|
{
|
||||||
"name": "MatterLabs Solidity Simple, Complex, and Semantic Tests",
|
"name": "MatterLabs Solidity Simple, Complex, and Semantic Tests",
|
||||||
"path": "$ABSOLUTE_PATH"
|
"paths": [
|
||||||
|
"$(realpath "$TEST_REPO_DIR/fixtures/solidity")"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
@@ -89,16 +91,19 @@ echo "This may take a while..."
|
|||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Run the tool
|
# Run the tool
|
||||||
RUST_LOG="info" cargo run --release -- execute-tests \
|
cargo build --release;
|
||||||
--platform geth-evm-solc \
|
RUST_LOG="info,alloy_pubsub::service=error" ./target/release/retester test \
|
||||||
--platform revive-dev-node-polkavm-resolc \
|
--platform revive-dev-node-polkavm-resolc \
|
||||||
--corpus "$CORPUS_FILE" \
|
--corpus "$CORPUS_FILE" \
|
||||||
--working-directory "$WORKDIR" \
|
--working-directory "$WORKDIR" \
|
||||||
--concurrency.number-of-nodes 5 \
|
--concurrency.number-of-nodes 10 \
|
||||||
|
--concurrency.number-of-threads 5 \
|
||||||
|
--concurrency.number-of-concurrent-tasks 500 \
|
||||||
|
--wallet.additional-keys 100000 \
|
||||||
--kitchensink.path "$SUBSTRATE_NODE_BIN" \
|
--kitchensink.path "$SUBSTRATE_NODE_BIN" \
|
||||||
--revive-dev-node.path "$REVIVE_DEV_NODE_BIN" \
|
--revive-dev-node.path "$REVIVE_DEV_NODE_BIN" \
|
||||||
--eth-rpc.path "$ETH_RPC_BIN" \
|
--eth-rpc.path "$ETH_RPC_BIN" \
|
||||||
> logs.log \
|
> logs.log \
|
||||||
2> output.log
|
2> output.log
|
||||||
|
|
||||||
echo -e "${GREEN}=== Test run completed! ===${NC}"
|
echo -e "${GREEN}=== Test run completed! ===${NC}"
|
||||||
Reference in New Issue
Block a user